public final class Analytics
{
	public enum Target
	{
		APP,
		// Add more trackers here if you need, and update the code in #get(Target) below
	}

	private static final String CONTENT_TYPE = "Experience";
	private static final String SCAN_EVENT = "scan_content";
	private static final String STAR_EVENT = "star_content";
	private static final String UNSTAR_EVENT = "unstar_content";
	private static final String OPEN_SCAN_EVENT = "open_content";
	private static FirebaseAnalytics analytics;

	static synchronized void initialize(Context context)
	{
		if (analytics != null)
		{
			throw new IllegalStateException("Extra call to initialize analytics trackers");
		}

		analytics = FirebaseAnalytics.getInstance(context);
	}

	public static void trackException(Throwable e)
	{
		Log.e("Google Analytics", e.getMessage(), e);
		//get(Target.APP).send(new HitBuilders.ExceptionBuilder()
		//		.setDescription(Throwables.getStackTraceAsString(e))
		//		.build());
	}

	public static void logScan(String experience, Action action)
	{
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, CONTENT_TYPE);
		bundle.putString(FirebaseAnalytics.Param.ITEM_ID, experience);
		bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, action.getName());
		//bundle.putString(FirebaseAnalytics.Param.);
		analytics.logEvent(SCAN_EVENT, bundle);
	}

	public static void logOpenScan(String experience, Action action)
	{
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, CONTENT_TYPE);
		bundle.putString(FirebaseAnalytics.Param.ITEM_ID, experience);
		bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, action.getName());
		bundle.putString("URL", action.getUrl());
		analytics.logEvent(OPEN_SCAN_EVENT, bundle);
	}

	public static void logStar(String experience) {
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, CONTENT_TYPE);
		bundle.putString(FirebaseAnalytics.Param.ITEM_ID, experience);
		analytics.logEvent(STAR_EVENT, bundle);
	}

	public static void logUnstar(String experience) {
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, CONTENT_TYPE);
		bundle.putString(FirebaseAnalytics.Param.ITEM_ID, experience);
		analytics.logEvent(UNSTAR_EVENT, bundle);
	}

	public static void logSelect(String experience)
	{
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, CONTENT_TYPE);
		bundle.putString(FirebaseAnalytics.Param.ITEM_ID, experience);
		analytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, bundle);
	}

	public static void logShare(String experience)
	{
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, CONTENT_TYPE);
		bundle.putString(FirebaseAnalytics.Param.ITEM_ID, experience);
		analytics.logEvent(FirebaseAnalytics.Event.SHARE, bundle);
	}

	public static void logSearch(String searchTerm)
	{
		Bundle bundle = new Bundle();
		bundle.putString(FirebaseAnalytics.Param.SEARCH_TERM, searchTerm);
		analytics.logEvent(FirebaseAnalytics.Event.SEARCH, bundle);
	}
} public final class Artcodes extends Application
{
	public static final String userAgent = "Artcodes/" + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + "; Android " + Build.VERSION.RELEASE + ")";
	private static final int cacheSize = 10 * 1024 * 1024; // 10MiB
	public static OkHttpClient httpClient;
	private ArtcodeServer server;

	@Override
	public void onCreate()
	{
		super.onCreate();
		Analytics.initialize(this);

		server = new AppEngineServer(this);
		if(httpClient == null)
		{
			HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
			logging.level(HttpLoggingInterceptor.Level.BASIC);

			httpClient = new OkHttpClient.Builder()
					.addInterceptor(logging)
					.cache(new Cache(getCacheDir(), cacheSize))
					.build();
		}
	}

	public ArtcodeServer getServer()
	{
		return server;
	}
} public enum Features implements Feature {
	show_welcome(true),
	show_local(false),
	log_scanned_images(true),
	edit_colour(false),
	edit_features(false),
	upload_to_artcodes_co_uk(false),
	open_without_touch(false);


	private final boolean defaultValue;

	Features(boolean defaultValue) {
		this.defaultValue = defaultValue;
	}

	@Override
	public String getName() {
		return name();
	}

	public boolean isEnabled(@NonNull Context context) {
		final SharedPreferences preferences = context.getSharedPreferences(Feature.class.getName(), Context.MODE_PRIVATE);
		final String featureName = name();
		if (preferences.contains(featureName)) {
			return preferences.getBoolean(featureName, defaultValue);
		}
		return defaultValue;
	}

	public void setEnabled(@NonNull Context context, boolean enabled) {
		final SharedPreferences preferences = context.getSharedPreferences(Feature.class.getName(), Context.MODE_PRIVATE);
		final String featureName = name();
		preferences.edit().putBoolean(featureName, enabled).apply();
	}
} public class Hash
{
	public static Map<String,String> salts;
	static
	{
		salts = new HashMap<>();
		salts.put("timehash1", "42ad0d1dz581a8ed");
	}
}
 public enum ScannerFeatures implements Feature {
	load_old_experiences(true),
	combined_markers(false);

	private final boolean defaultValue;

	ScannerFeatures(boolean defaultValue) {
		this.defaultValue = defaultValue;
	}

	@Override
	public String getName() {
		return name();
	}

	public boolean isEnabled(@NonNull Context context) {
		final SharedPreferences preferences = context.getSharedPreferences(Feature.class.getName(), Context.MODE_PRIVATE);
		final String featureName = name();
		if (preferences.contains(featureName)) {
			return preferences.getBoolean(featureName, defaultValue);
		}
		return defaultValue;
	}

	public void setEnabled(@NonNull Context context, boolean enabled) {
		final SharedPreferences preferences = context.getSharedPreferences(Feature.class.getName(), Context.MODE_PRIVATE);
		final String featureName = name();
		preferences.edit().putBoolean(featureName, enabled).apply();
	}
} public class ArtcodeDetector extends Detector
{
	private static final Map<String, ImageProcessorFactory> factoryRegistry = new HashMap<>();

	static
	{
		register(new MarkerDetector.Factory());
		register(new MarkerEmbeddedChecksumDetector.Factory());
		register(new MarkerAreaOrderDetector.Factory());
		register(new MarkerEmbeddedChecksumAreaOrderDetector.Factory());

		register(new TileThresholder.Factory());
		register(new OtsuThresholder.Factory());

		register(new IntensityFilter.IntensityFilterFactory());
		register(new Inverter.InverterFactory());
		register(new WhiteBalanceImageProcessor.WhiteBalanceImageProcessorFactory());
		register(new HlsEditImageProcessor.HlsEditImageProcessorFactory());
		register(new RgbColourFilter.RedFactory());
		register(new RgbColourFilter.GreenFactory());
		register(new RgbColourFilter.BlueFactory());
		register(new CmykColourFilter.CyanCmykColourFilterFactory());
		register(new CmykColourFilter.MagentaCmykColourFilterFactory());
		register(new CmykColourFilter.YellowCmykColourFilterFactory());
		register(new CmykColourFilter.BlackCmykColourFilterFactory());
	}

	public ArtcodeDetector(final Context context, Experience experience, MarkerDetectionHandler handler, CameraFocusControl cameraFocusControl)
	{

		this.fullscreen = experience.getFullscreen() != null && experience.getFullscreen();

		boolean missingProcessors = false;

		Log.i("ArtcodesDetector", "Pipeline items: "+experience.getPipeline().size());

		for (String processorName : experience.getPipeline())
		{
			ImageProcessor processor = getProcessor(context, processorName, experience, handler, cameraFocusControl);
			if (processor != null)
			{
				pipeline.add(processor);
			}
			else
			{
				missingProcessors = true;
				Log.w("ArtcodeDetector", "Missing pipeline item: "+processorName);
			}
		}

		if (missingProcessors)
		{
			new AlertDialog.Builder(context)
					.setTitle("Hmm...")
					.setMessage("This experience uses features not in this version of Artcodes. It might work fine or you can check Google Play for updates.")
					.setPositiveButton("Update", new DialogInterface.OnClickListener() {
						public void onClick(DialogInterface dialog, int which) {
							final String appPackageName = context.getPackageName();
							try {
								context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName)));
							} catch (android.content.ActivityNotFoundException e) {
								context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName)));
							}
						}
					})
					.setNegativeButton("Continue", new DialogInterface.OnClickListener() {
						public void onClick(DialogInterface dialog, int which) {
							// do nothing
						}
					})
					.setIcon(android.R.drawable.ic_dialog_alert)
					.show();

		}

		if (pipeline.isEmpty())
		{
			pipeline.add(new TileThresholder());
			pipeline.add(new MarkerDetector(experience, handler));
		}

		if (experience.getRequestedAutoFocusMode() != null && experience.getRequestedAutoFocusMode().equals("blurScore"))
		{
			pipeline.add(0, new BlurDetectionFilter(cameraFocusControl));
		}
	}

	private static void register(ImageProcessorFactory factory)
	{
		factoryRegistry.put(factory.getName(), factory);
	}

	private static ImageProcessor getProcessor(Context context, String string, Experience experience, MarkerDetectionHandler handler, CameraFocusControl cameraFocusControl)
	{
		// matches "group1" or "group1(group2)" or "group1()"
		String pattern = "([^()]+)(?:\\(([^()]*)\\))?";
		Pattern r = Pattern.compile(pattern);
		Matcher m = r.matcher(string);

		if (m.find())
		{
			String imageProcessorName = m.group(1);
			String imageProcessorArgs = m.group(2);
			Log.i("ArtcodeDetector", "Attempting to create image processor '"+imageProcessorName+"' with args '"+imageProcessorArgs+"'");
			ImageProcessorFactory factory = factoryRegistry.get(imageProcessorName);
			if (factory != null)
			{
				try
				{
					return factory.create(context, experience, handler, cameraFocusControl, getImageProcessorArgs(imageProcessorArgs));
				}
				catch (Exception e)
				{
					Log.w("detector", e.getMessage(), e);
				}
			}
		}
		else
        {
            Log.i("ArtcodeDetector", "Regex error: '"+pattern+"' did not match '"+string+"'");
        }

		return null;
	}

	/**
	 * Creates a Map from the input.
	 * @param fromString A string of key value pairs (note keys don't need values) in format: "key1=value1,key2,key3=value3".
	 * @return A Map
     */
	private static Map<String,String> getImageProcessorArgs(String fromString)
	{
		Map<String,String> imageProcessorArgs = new HashMap<>();
		if (fromString != null)
		{
			String[] args = fromString.split(",");
			for (String arg : args)
			{
				String[] argArray = arg.trim().split("=");
				if (argArray.length==1)
				{
					imageProcessorArgs.put(argArray[0], argArray[0]);
				}
				else if (argArray.length>=2)
				{
					imageProcessorArgs.put(argArray[0].trim(), argArray[1].trim());
				}
			}
		}
		return imageProcessorArgs;
	}

	@Override
	protected Rect createROI(int imageWidth, int imageHeight, int surfaceWidth, int surfaceHeight)
	{
		final int size = Math.min(imageWidth, imageHeight);

		final int colStart = (imageWidth - size) / 2;
		final int rowStart = (imageHeight - size) / 2;

		final float surfaceMax = Math.max(surfaceHeight, surfaceWidth);

		float sizeRatio = surfaceMax / Math.max(imageWidth, imageHeight);
		Log.i("Detector", "Size ratio = " + sizeRatio);

		if(callback != null)
		{
			int margin = (int) (Math.max(colStart, rowStart) * sizeRatio);
			Log.i("Detector", "Size = " + size + ", margin = " + margin);
			callback.detectionStart(margin);
		}
		return new Rect(colStart, rowStart, size, size);
	}
} public interface ActionDetectionHandler
{
	/**
	 * Called when markers that trigger an Action are detected (or markers that may make up a future group/sequential Action).
	 * Note: detectedAction.codes() should always be a sub-list of possibleFutureAction.codes().
	 *
	 * @param detectedAction        The detected Action (can be null if detected markers are only part of a group/sequential action).
	 * @param possibleFutureAction  A possible future action that might be triggered by markers detected so far.
	 * @param imagesForFutureAction Details of markers just detected.
	 */
	void onMarkerActionDetected(Action detectedAction, Action possibleFutureAction, List<MarkerImage> imagesForFutureAction);
} public class MarkerActionDetectionHandler extends MarkerCodeDetectionHandler
{
	protected static final int REQUIRED = 5;
	protected static final int MAX = REQUIRED;
	// Keep displayed for 10s
	private static final int REMAIN = 10000;

	private final ActionDetectionHandler markerActionHandler;
	private final Experience experience;
	private final MarkerDrawer markerDrawer;
	private Action currentAction;
	private long lastSeen;

	public MarkerActionDetectionHandler(ActionDetectionHandler markerActionHandler, Experience experience, MarkerDrawer markerDrawer)
	{
		super(experience, null);
		this.markerActionHandler = markerActionHandler;
		this.experience = experience;
		this.markerDrawer = markerDrawer;
	}

	@Override
	public void onMarkersDetected(Collection<Marker> markers, ArrayList<MatOfPoint> contours, Mat hierarchy, Size sourceImageSize)
	{
		countMarkers(markers);
		long now = System.currentTimeMillis();
		int best = 0;
		Action selected = null;
		for (Action action : this.experience.getActions())
		{
			if (action.getMatch() == Action.Match.any)
			{
				for (String code : action.getCodes())
				{
					int count = markerCounts.count(code);
					if (count > best)
					{
						selected = action;
						best = count;
					}
				}
			}
			else if (action.getMatch() == Action.Match.all)
			{
				int min = MAX;
				int total = 0;
				for (String code : action.getCodes())
				{
					int count = markerCounts.count(code);
					min = Math.min(min, count);
					total += (count * 2);
				}

				if (min > REQUIRED && total > best)
				{
					best = total;
					selected = action;
				}
			}
		}

		if (best < REQUIRED)
		{
			if (currentAction != null)
			{
				if(now - lastSeen > REMAIN)
				{
					currentAction = null;
					this.markerActionHandler.onMarkerActionDetected(null, null, null);
				}
			}
		}
		else if (selected != currentAction)
		{
			currentAction = selected;
			lastSeen = now;
			ArrayList<MarkerImage> markerImages = null;
			if (this.markerDrawer != null)
			{
				Marker markerObject = null;
				for (Marker possibleMarkerObject : markers)
				{
					if (possibleMarkerObject.toString().equals(currentAction.getCodes().get(0)))
					{
						markerObject = possibleMarkerObject;
					}
				}
				if (markerObject != null)
				{
					final Rect boundingRect = Imgproc.boundingRect(contours.get(markerObject.markerIndex));
					Mat thumbnailMat = this.markerDrawer.drawMarker(markerObject, contours, hierarchy, boundingRect, null);
					Bitmap thumbnail = Bitmap.createBitmap(thumbnailMat.width(), thumbnailMat.height(), Bitmap.Config.ARGB_8888);
					Utils.matToBitmap(thumbnailMat, thumbnail);
					MarkerImage markerImage = new MarkerImage(markerObject.toString(), thumbnail, (float) (boundingRect.tl().x / sourceImageSize.width), (float) (boundingRect.tl().y / sourceImageSize.height), (float) (boundingRect.width / sourceImageSize.width), (float) (boundingRect.height / sourceImageSize.height));
					markerImages = new ArrayList<>(1);
					markerImages.add(markerImage);

					Log.i("SOURCEIMG", "w" + sourceImageSize.width + " h" + sourceImageSize.height);
				}
			}
			this.markerActionHandler.onMarkerActionDetected(currentAction, currentAction, markerImages);
		}
		else
		{
			for (Marker possibleMarkerObject : markers)
			{
				String marker = possibleMarkerObject.toString();
				for(String code: currentAction.getCodes())
				{
					if(code.equals(marker))
					{
						lastSeen = now;
						return;
					}
				}
			}
		}
	}
} public class MultipleMarkerActionDetectionHandler extends MarkerCodeDetectionHandler
{
    /**
     * A class that holds the details of a marker's detection state
     */
    private static class MarkerDetectionRecord {
        static int instanceCount = 0;
        final int instanceId;
        final String code;
        final Marker marker;
        long firstDetected;
        long lastDetected;
        int count;
        MarkerImage markerImage;

        public MarkerDetectionRecord(Marker marker)
        {
            this.marker = marker;
            this.code = marker.toString();
            this.count = 0;
            this.instanceId = instanceCount++;
        }

        public MarkerDetectionRecord clone(Marker marker)
        {
            MarkerDetectionRecord clone = new MarkerDetectionRecord(marker==null?this.marker:marker);
            clone.firstDetected = this.firstDetected;
            clone.lastDetected = this.lastDetected;
            clone.count = this.count;
            clone.markerImage = this.markerImage;
            return clone;
        }

        @Override
        public int hashCode()
        {
            return this.instanceId;
        }

        @Override
        public boolean equals(Object o)
        {
            if (o instanceof MarkerDetectionRecord)
            {
                return this.instanceId == ((MarkerDetectionRecord) o).instanceId;
            }
            return false;
        }

        @Override
        public String toString()
        {
            return "<#"+instanceId+" "+code+" x"+count+">";
        }
    }

    protected final ActionDetectionHandler markerActionHandler;
    protected final Experience experience;
    protected final MarkerDrawer markerDrawer;


    protected static final int REQUIRED = 5;
    protected static final int MAX = REQUIRED*4;

    protected long lastAddedToHistory = 0;
    protected boolean shouldClearHistoryOnReset = true;

    protected List<MarkerDetectionRecord> mDetectionHistory = new ArrayList<>();
    protected List<String> mCodesDetected = new ArrayList<>();
    protected Map<String, MarkerDetectionRecord> mActiveMarkerRecoreds = new HashMap<>();

    public MultipleMarkerActionDetectionHandler(ActionDetectionHandler markerActionHandler, Experience experience, MarkerDrawer markerDrawer)
    {
        super(experience, null);
        this.markerActionHandler = markerActionHandler;
        this.experience = experience;
        this.markerDrawer = markerDrawer;
    }

    @Override
    public void onMarkersDetected(Collection<Marker> markers, ArrayList<MatOfPoint> contours, Mat hierarchy, Size sourceImageSize)
    {
        addMarkers(markers, contours, hierarchy, sourceImageSize);
        actOnMarkers();
    }

    private MarkerImage createImageForMarker(Marker marker, ArrayList<MatOfPoint> contours, Mat hierarchy, Size sourceImageSize)
    {
        if (marker != null)
        {
            final Rect boundingRect = Imgproc.boundingRect(contours.get(marker.markerIndex));
            final Mat thumbnailMat = this.markerDrawer.drawMarker(marker, contours, hierarchy, boundingRect, null);
            final Bitmap thumbnail = Bitmap.createBitmap(thumbnailMat.width(), thumbnailMat.height(), Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(thumbnailMat, thumbnail);
            return new MarkerImage(marker.toString(), thumbnail, (float) (boundingRect.tl().x / sourceImageSize.width), (float) (boundingRect.tl().y / sourceImageSize.height), (float) (boundingRect.width / sourceImageSize.width), (float) (boundingRect.height / sourceImageSize.height));
        }
        return null;
    }

    public void reset()
    {
        mActiveMarkerRecoreds.clear();
        mCodesDetected.clear();
        if (shouldClearHistoryOnReset)
        {
            mDetectionHistory.clear();
        }
        existingAction = null;
        existingThumbnails = null;
        existingFutureAction = null;
        this.markerActionHandler.onMarkerActionDetected(null, null, null);
    }

    public void addMarkers(Collection<Marker> markers, ArrayList<MatOfPoint> contours, Mat hierarchy, Size sourceImageSize)
    {
        long time = System.currentTimeMillis();

        // Process markers detected on this frame
        for (Marker marker : markers)
        {
            String code = marker.toString();

            MarkerDetectionRecord markerDetectionRecord = mActiveMarkerRecoreds.get(code);
            if (markerDetectionRecord == null)
            {
                // New marker: add it to data structure
                markerDetectionRecord = new MarkerDetectionRecord(marker);
                mActiveMarkerRecoreds.put(code, markerDetectionRecord);
            }

            int countIncrease = marker instanceof MarkerWithEmbeddedChecksum ? REQUIRED-1 : 1;
            // add to history (if it has passed the required count on this frame)
            if (markerDetectionRecord.count < REQUIRED && markerDetectionRecord.count + countIncrease >= REQUIRED)
            {
                // don't add duplicates to history unless enough time has passed
                if (this.mDetectionHistory.isEmpty() || System.currentTimeMillis() - this.lastAddedToHistory >= 1000 || !code.equals(this.mDetectionHistory.get(this.mDetectionHistory.size() - 1).code))
                {
                    if (markerDetectionRecord.markerImage!=null)
                    {
                        // if second time this marker is detected
                        // create new entry and leave old one in history
                        markerDetectionRecord.markerImage.newDetection = false;
                        markerDetectionRecord.markerImage.detectionActive = false;
                        markerDetectionRecord = markerDetectionRecord.clone(marker);
                        mActiveMarkerRecoreds.put(code, markerDetectionRecord);
                    }
                    markerDetectionRecord.firstDetected = time;
                    mDetectionHistory.add(markerDetectionRecord);
                    this.lastAddedToHistory = time;
                    mCodesDetected.add(markerDetectionRecord.code);
                }
                markerDetectionRecord.markerImage = createImageForMarker(marker, contours, hierarchy, sourceImageSize);
                markerDetectionRecord.markerImage.newDetection = true;
            }
            else if (markerDetectionRecord.markerImage != null)
            {
                markerDetectionRecord.markerImage.newDetection = false;
            }

            // increase its count
            markerDetectionRecord.count = Math.min(markerDetectionRecord.count + countIncrease, MAX);
            markerDetectionRecord.lastDetected = time;
        }

        // Workout which markers have timed out:
        List<String> toRemove = new ArrayList<>();
        for(MarkerDetectionRecord markerRecord: mActiveMarkerRecoreds.values())
        {
            if(!markers.contains(markerRecord.marker))
            {
                if (markerRecord.count == REQUIRED)
                {
                    mCodesDetected.remove(markerRecord.code);
                    if (markerRecord.markerImage != null)
                    {
                        markerRecord.markerImage.detectionActive = false;
                        markerRecord.markerImage.newDetection = false;
                    }
                }
                else if (markerRecord.count <= 1)
                {
                    toRemove.add(markerRecord.code);
                    continue;
                }
                markerRecord.count = markerRecord.count - 1;
            }
        }
        for (String markerToRemove : toRemove)
        {
            mActiveMarkerRecoreds.remove(markerToRemove);
        }
        Collections.sort(mCodesDetected);
    }

    private void actOnMarkers()
    {
        final String standardCode =  getStandardCode();
        final Action action = getActionFor(standardCode);

        final Action sequentialAction = getActionFor(getSequentialCode());
        final Action futureSequentialAction = getPossibleFutureSequentialActionFor(sequentialAction==null?action:sequentialAction, standardCode);
        if (sequentialAction!=null)
        {
            sendIfResultChanged(sequentialAction, futureSequentialAction, getImagesForAction(futureSequentialAction));
            return;
        }

        final Action groupAction = getActionFor(getGroupCode());
        final Action futureGroupAction = getPossibleFutureGroupActionFor(groupAction==null?action:groupAction);
        if (groupAction!=null)
        {
            sendIfResultChanged(groupAction, futureGroupAction, getImagesForAction(futureGroupAction));
            return;
        }

        final Action futureAction = futureSequentialAction!=action?futureSequentialAction:futureGroupAction;
        sendIfResultChanged(action, futureAction, getImagesForAction(futureAction));
    }

    private Action existingAction = null, existingFutureAction = null;
    private List<MarkerImage> existingThumbnails = null;
    private void sendIfResultChanged(Action action, Action futureAction, List<MarkerImage> thumbnails)
    {
        if (((existingAction!=null && !existingAction.equals(action)) || (action!=null && !action.equals(existingAction))) ||
                ((existingThumbnails!=null && !existingThumbnails.equals(thumbnails)) || (thumbnails!=null && !thumbnails.equals(existingThumbnails))) ||
                ((existingFutureAction!=null && !existingFutureAction.equals(futureAction)) || ((futureAction!=null && !futureAction.equals(existingFutureAction)))))
        {
            this.existingAction = action;
            this.existingThumbnails = thumbnails;
            this.existingFutureAction = futureAction;
            this.markerActionHandler.onMarkerActionDetected(action, futureAction, thumbnails);
        }

    }

    private List<MarkerImage> getImagesForAction(Action action)
    {
        if (action!=null)
        {
            List<MarkerImage> result = new ArrayList<>(action.getCodes().size());
            if (action.getMatch() == Action.Match.any)
            {
                for (String code : action.getCodes())
                {
                    MarkerDetectionRecord record = mActiveMarkerRecoreds.get(code);
                    if (record != null && record.markerImage != null && record.markerImage.detectionActive)
                    {
                        result.add(record.markerImage);
                        return result;
                    }
                }
            }
            else if (action.getMatch() == Action.Match.all)
            {
                for (String code : action.getCodes())
                {
                    MarkerDetectionRecord record = mActiveMarkerRecoreds.get(code);
                    if (record != null && record.markerImage != null && record.markerImage.detectionActive)
                    {
                        result.add(record.markerImage);
                    }
                    else
                    {
                        result.add(null);
                    }
                }
                return result;
            }
            else if (action.getMatch() == Action.Match.sequence)
            {
                List<String> historyAsStrings = new ArrayList<>();
                for (MarkerDetectionRecord record : mDetectionHistory)
                {
                    historyAsStrings.add(record.code);
                }

                for (int numberOfCodesInHistory = Math.min(action.getCodes().size(), historyAsStrings.size()); numberOfCodesInHistory>0; --numberOfCodesInHistory)
                {
                    if (firstN(action.getCodes(), numberOfCodesInHistory).equals(lastN(historyAsStrings, numberOfCodesInHistory)))
                    {
                        int start = mDetectionHistory.size() - numberOfCodesInHistory;
                        for (MarkerDetectionRecord record : mDetectionHistory.subList(start<0?0:start, mDetectionHistory.size()))
                        {
                            result.add(record.markerImage);
                        }
                        for (int i=numberOfCodesInHistory; i<action.getCodes().size(); ++i)
                        {
                            result.add(null);
                        }
                        break;
                    }
                }
                return result;
            }
            return result;
        }
        return null;
    }

    private List<String> firstN(List<String> list, int n)
    {
        int start = list.size() - n;
        return list.subList(0, n>list.size()?list.size():n);
    }
    private List<String> lastN(List<String> list, int n)
    {
        int start = list.size() - n;
        return list.subList(start<0?0:start, list.size());
    }

    /**
     * Search for group codes (or "pattern groups") in the detected codes. This will only return
     * group codes set in the experience.
     * @return
     */
    private String getGroupCode() {
        if (experience != null) {
            // Search for pattern groups
            // By getting every combination of the currently detected markers and checking if they exist in the experience (biggest groups first, groups must include at least 2 markers)
            if (mCodesDetected != null && mCodesDetected.size() > 1) {
                List<Set<List<String>>> combinations = new ArrayList<>();
                combinationsOfStrings(mCodesDetected, mCodesDetected.size(), combinations);
                for (int i = combinations.size() - 1; i >= 1; --i) {
                    List<String> mostRecentGroup = null;
                    String mostRecentGroupStr = null;
                    for (List<String> code : combinations.get(i)) {
                        String codeStr = joinStr(code, "+");
                        if (isValidCode(codeStr) && doMarkerDetectionTimesOverlap(code) && getMostRecentDetectionTime(code, mostRecentGroup)>getMostRecentDetectionTime(mostRecentGroup, code)) {
                            mostRecentGroup = code;
                            mostRecentGroupStr = codeStr;
                        }
                    }
                    if (mostRecentGroup!=null)
                    {
                        return mostRecentGroupStr;
                    }
                }
            }
        }
        return null;
    }

    private long getMostRecentDetectionTime(List<String> codes, List<String> excluding)
    {
        long mostRecentTime = 0;
        if (codes!=null)
        {
            for (String codeStr : codes)
            {
                if (excluding == null || !excluding.contains(codeStr))
                {
                    MarkerDetectionRecord code = mActiveMarkerRecoreds.get(codeStr);
                    if (code != null && code.lastDetected > mostRecentTime)
                    {
                        mostRecentTime = code.lastDetected;
                    }
                }
            }
        }
        return mostRecentTime;
    }

    private boolean doMarkerDetectionTimesOverlap(List<String> codes)
    {
        for (int i=0; i<codes.size()-1; ++i)
        {
            MarkerDetectionRecord code1 = mActiveMarkerRecoreds.get(codes.get(i));
            boolean overlapFound = false;
            for (int j=i+1; j<codes.size(); ++j)
            {
                MarkerDetectionRecord code2 = mActiveMarkerRecoreds.get(codes.get(j));
                overlapFound = doTimesOverlap(code1.firstDetected, code1.lastDetected, code2.firstDetected, code2.lastDetected);
                if (overlapFound)
                {
                    break;
                }
            }
            if (!overlapFound)
            {
                return false;
            }
        }
        return true;
    }

    private static boolean doTimesOverlap(long firstDetected1, long lastDetected1, long firstDetected2, long lastDetected2)
    {
        return (firstDetected1 <= lastDetected2)  &&  (lastDetected1 >= firstDetected2);
    }

    /**
     * Get all the combinations of objects up to a maximum size for the combination and add it to the result List.
     * E.g. combinationsOfStrings([1,3,2], 2, []) changes the result array to [([1],[2],[3]),([1,2],[1,3],[2,3])] where () denotes a Set and [] denotes a List.
     */
    private static void combinationsOfStrings(List<String> strings, int maxCombinationSize, List<Set<List<String>>> result)
    {

        if (maxCombinationSize > 0)
        {
            if (maxCombinationSize == 1)
            {
                Set<List<String>> resultForN = new HashSet<>();
                for (String code : strings)
                {
                    List<String> tmp = new ArrayList<>();
                    tmp.add(code);
                    resultForN.add(tmp);
                }
                result.add(resultForN);
            }
            else if (maxCombinationSize == strings.size())
            {
                combinationsOfStrings(strings, maxCombinationSize - 1, result);
                Set<List<String>> resultForN = new HashSet<>();
                Collections.sort(strings);
                resultForN.add(strings);
                result.add(resultForN);
            }
            else
            {
                Set<List<String>> resultForN = new HashSet<>();
                combinationsOfStrings(strings, maxCombinationSize - 1, result);
                Set<List<String>> base = result.get(result.size() - 1);

                for (String code : strings)
                {
                    for (List<String> setMinus1 : base)
                    {
                        if (!setMinus1.contains(code))
                        {
                            List<String> aResult = new ArrayList<>(setMinus1);
                            aResult.add(code);
                            Collections.sort(aResult);
                            resultForN.add(aResult);
                        }
                    }
                }


                result.add(resultForN);
            }
        }
    }

    /**
     * Search for sequential codes (or "pattern paths") in detection history. This method may
     * remove items from history that do not match the beginning of any sequential code in the
     * experience and will only return a code from the experience.
     * @return
     */
    private String getSequentialCode()
    {
        if (experience != null)
        {
            // Search for sequential actions in history
            // by creating history sub-lists and checking if any codes in the experience match.
            // e.g. if history=[A,B,C,D] check sub-lists [A,B,C,D], [B,C,D], [C,D].
            if (mDetectionHistory != null && mDetectionHistory.size() > 0)
            {
                boolean foundPrefix = false;
                int start = 0;

                List<String> detectionHistoryAsStrings = new ArrayList<>();
                for (MarkerDetectionRecord record : mDetectionHistory)
                {
                    detectionHistoryAsStrings.add(record.code);
                }
                while (start < mDetectionHistory.size())
                {
                    List<String> subList = detectionHistoryAsStrings.subList(start, detectionHistoryAsStrings.size());
                    String joinedString = joinStr(subList, ">");
                    if (subList.size() != 1 && isValidCode(joinedString))
                    {
                        // Case 1: The history sublist is a sequential code in the experience.
                        return joinedString;
                    }
                    else if (!foundPrefix && !hasSequentialPrefix(joinedString))
                    {
                        // Case 2: No sequential codes in the experience start with the history sublist (as well as previous history sublists).
                        // So remove the first part of it from history
                        // This ensures that history never grows longer than the longest code
                        detectionHistoryAsStrings.remove(0);
                        mDetectionHistory.remove(0);
                        start = 0;
                    }
                    else
                    {
                        // Case 3: Sequential codes in the experience start with the history sublist (or a previous history sublist).
                        foundPrefix = true;
                        start++;
                    }
                }
            }
        }
        return null;
    }

    private static String joinStr(Collection<String> strings, String joiner)
    {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String string : strings)
        {
            if (!first)
            {
                sb.append(joiner);
            }
            sb.append(string);
            first=false;
        }
        return sb.toString();
    }

    /**
     * Search for the single marker with the highest count that is in the experience, or just the highest count if none are in the experience.
     * @return
     */
    private String getStandardCode()
    {
        MarkerDetectionRecord result = null;
        boolean resultIsInExperience = false;
        for (String code : mCodesDetected)
        {
            MarkerDetectionRecord marker = mActiveMarkerRecoreds.get(code);
            boolean markerIsInExperience = isValidCode(code);
            if (result==null || (!resultIsInExperience && markerIsInExperience) || (resultIsInExperience==markerIsInExperience && ((marker.lastDetected>result.lastDetected)||(marker.lastDetected==result.lastDetected && marker.firstDetected>result.firstDetected)||(marker.lastDetected==result.lastDetected && marker.firstDetected==result.firstDetected && marker.count>result.count))))
            {
                result = marker;
                resultIsInExperience = markerIsInExperience;
            }
        }

        return result==null ? null : result.code;
    }

    private HashMap<String, Action> validCodes = null;
    private HashMap<String, Set<Action>> subGroupCodes = null;
    private HashMap<String, Set<Action>> subSequenceCodes = null;

    private void logDataCache()
    {
        Log.i("DATACACHE", "Valid codes = " + joinStr(validCodes.keySet(), ", "));

        List<String> subGroupCodesStrs = new ArrayList<>();
        for (Map.Entry<String, Set<Action>> entry : subGroupCodes.entrySet())
        {
            List<String> actionStrs = new ArrayList<>();
            for (Action action : entry.getValue())
            {
                actionStrs.add(joinStr(action.getCodes(), ","));
            }
            subGroupCodesStrs.add(entry.getKey() + ": " + joinStr(actionStrs, " or "));
        }
        Log.i("DATACACHE", "Sub-group codes = " + joinStr(subGroupCodesStrs, ", "));

        subGroupCodesStrs = new ArrayList<>();
        for (Map.Entry<String, Set<Action>> entry : subSequenceCodes.entrySet())
        {
            List<String> actionStrs = new ArrayList<>();
            for (Action action : entry.getValue())
            {
                actionStrs.add(joinStr(action.getCodes(), ","));
            }
            subGroupCodesStrs.add(entry.getKey() + ": " + joinStr(actionStrs, " or "));
        }
        Log.i("DATACACHE", "Sub-sequence codes = " + joinStr(subGroupCodesStrs, ", "));
    }

    private boolean isValidCode(String code)
    {
        if (validCodes==null)
        {
            createDataCache();
        }
        return validCodes.containsKey(code);
    }

    private boolean hasSequentialPrefix(String prefix)
    {
        if (subSequenceCodes==null)
        {
            createDataCache();
        }
        return subSequenceCodes.containsKey(prefix);
    }

    private Action getActionFor(String code)
    {
        if (validCodes==null)
        {
            createDataCache();
        }
        return validCodes.get(code);
    }

    private Action getPossibleFutureSequentialActionFor(Action found, String foundUsing)
    {
        if (subSequenceCodes == null)
        {
            createDataCache();
        }

        int minimumSize = 1;
        if (found != null && found.getMatch() != Action.Match.any)
        {
            minimumSize = found.getCodes().size() + 1;
        }

        if (mDetectionHistory.size()==0)
        {
            return found;
        }

        // if a single marker triggered found Action and it's not the last one in history then do
        // not provide a possible future sequential action as this will look confusing in the interface
        if (found!=null && found.getMatch()==Action.Match.any && foundUsing!=null)
        {
            MarkerDetectionRecord last = mDetectionHistory.get(mDetectionHistory.size()-1);
            if (!foundUsing.equals(last.code))
            {
                return found;
            }
        }

        if (found == null || found.getMatch() != Action.Match.all)
        {
            List<String> detectionHistoryAsStrings = new ArrayList<>();
            for (MarkerDetectionRecord record : mDetectionHistory)
            {
                detectionHistoryAsStrings.add(record.code);
            }
            for (int i = 0; i < detectionHistoryAsStrings.size(); ++i)
            {
                List<String> subHistory = detectionHistoryAsStrings.subList(i, detectionHistoryAsStrings.size());
                Set<Action> actions = subSequenceCodes.get(joinStr(subHistory, ">"));
                if (actions != null && !actions.isEmpty())
                {
                    Action longestSequentialAction = null;
                    for (Action action : actions)
                    {
                        if (action.getCodes().size() >= minimumSize && (longestSequentialAction == null || longestSequentialAction.getCodes().size() < action.getCodes().size()))
                        {
                            longestSequentialAction = action;
                        }
                    }
                    if (longestSequentialAction != null)
                    {
                        return longestSequentialAction;
                    }
                }
            }
        }

        return found;
    }

    private Action getPossibleFutureGroupActionFor(Action found)
    {
        if (subGroupCodes == null)
        {
            createDataCache();
        }

        if (found == null || found.getMatch()!=Action.Match.sequence)
        {

            List<String> detectedInFound = null;
            if (found!=null)
            {
                detectedInFound = intersection(found.getCodes(), mCodesDetected);
            }

            Set<Action> groupFutureActions = subGroupCodes.get(joinStr(mCodesDetected, "+"));
            if (groupFutureActions != null && !groupFutureActions.isEmpty())
            {
                Action largestGroupAction = null;
                for (Action action : groupFutureActions)
                {
                    if ((found==null || action.getCodes().containsAll(detectedInFound)) && (largestGroupAction == null || largestGroupAction.getCodes().size() < action.getCodes().size()))
                    {
                        largestGroupAction = action;
                    }
                }
                if (largestGroupAction!=null)
                {
                    return largestGroupAction;
                }
            }
        }

        return found;
    }

    private static List<String> intersection(List<String> list1, List<String> list2)
    {
        List<String> intersection = new ArrayList<>();
        if (list1!=null && list2!=null)
        {
            intersection.addAll(list1);
            intersection.retainAll(list2);
        }
        return intersection;
    }

    private void createDataCache()
    {
        if (validCodes==null)
        {
            validCodes = new HashMap<>();
            subGroupCodes = new HashMap<>();
            subSequenceCodes = new HashMap<>();
            for (Action action : experience.getActions())
            {
                if (action.getMatch()== Action.Match.any || action.getCodes().size()==1) // single
                {
                    for (String code : action.getCodes())
                    {
                        validCodes.put(code, action);
                    }
                }
                else if (action.getMatch()== Action.Match.all) // group
                {
                    String code = joinStr(action.getCodes(), "+");
                    validCodes.put(code, action);

                    List<Set<List<String>>> subGroupsByLength = new ArrayList<>();
                    combinationsOfStrings(action.getCodes(), action.getCodes().size()-1, subGroupsByLength);
                    for (Set<List<String>> setOfGroups : subGroupsByLength)
                    {
                        for (List<String> group : setOfGroups)
                        {
                            code = joinStr(group, "+");
                            Set<Action> actions = subGroupCodes.get(code);
                            if (actions != null)
                            {
                                actions.add(action);
                            }
                            else
                            {
                                HashSet<Action> actionsForSubGroup = new HashSet<>();
                                actionsForSubGroup.add(action);
                                subGroupCodes.put(code, actionsForSubGroup);
                            }
                        }
                    }
                }
                else if (action.getMatch()== Action.Match.sequence)
                {
                    String code = joinStr(action.getCodes(), ">");
                    validCodes.put(code, action);
                    for (int subCodeSize=1; subCodeSize<action.getCodes().size(); ++subCodeSize)
                    {
                        code = joinStr(action.getCodes().subList(0, subCodeSize), ">");
                        Set<Action> actions = subSequenceCodes.get(code);
                        if (actions != null)
                        {
                            actions.add(action);
                        }
                        else
                        {
                            HashSet<Action> actionsForSubSequence = new HashSet<>();
                            actionsForSubSequence.add(action);
                            subSequenceCodes.put(code, actionsForSubSequence);
                        }
                    }
                }
            }
        }
    }
} public class MarkerThumbnailDrawer implements MarkerDrawer
{
    private static final Scalar COLOR = new Scalar(255,255,255,255);
    private static final Scalar BACKGROUND = new Scalar(0,0,0,0);

    @Override
    public Mat drawMarker(Marker marker, ArrayList<MatOfPoint> contours, Mat hierarchy, Rect boundingRect, Mat imageToDrawOn)
    {
        if (imageToDrawOn==null)
        {
            if (boundingRect==null)
            {
                boundingRect = Imgproc.boundingRect(contours.get(marker.markerIndex));
            }
            Mat output = new Mat(boundingRect.size(), CvType.CV_8UC4, BACKGROUND);

            Imgproc.drawContours(output, contours, marker.markerIndex, COLOR, Core.FILLED, Core.LINE_8, hierarchy, 2, new Point(-boundingRect.tl().x, -boundingRect.tl().y));

            return output;
        }
        else
        {
            Imgproc.drawContours(imageToDrawOn, contours, marker.markerIndex, COLOR, Core.FILLED, Core.LINE_8, hierarchy, 2, new Point(0, 0));
            return imageToDrawOn;
        }
    }
} public class Action
{
	public enum Match
	{
		any, all, sequence
	}

	/**
	 * Action level option for the "visual checksum (optional)" detector.
	 */
	public enum ChecksumOption
	{
		optional, required, excluded
	}

	public static final String HTTP_PREFIX = "http://";
	private final List<String> codes = new ArrayList<>();
	private Match match = Match.any;
	private String url;
	private String name;
	private String description;
	private String image;
	private String owner;
	//private boolean showDetail = false;

	private Integer framesRequired;
	private Integer framesAwarded;
	private Double minimumSize;

	private ChecksumOption checksumOption;

	public List<String> getCodes()
	{
		return codes;
	}

	public String getDescription()
	{
		return description;
	}

	public String getOwner()
	{
		return owner;
	}

	public void setDescription(String description)
	{
		this.description = description;
	}

	public String getImage()
	{
		return image;
	}

	public void setImage(String image)
	{
		this.image = image;
	}

	public Match getMatch()
	{
		return match;
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

//	public boolean getShowDetail()
//	{
//		return showDetail;
//	}

	public String getUrl()
	{
		return url;
	}

	public void setOwner(String owner)
	{
		this.owner = owner;
	}

	public void setUrl(String url)
	{
		this.url = url;
	}

	public String getDisplayUrl()
	{
		if (url != null && url.startsWith(HTTP_PREFIX))
		{
			return url.substring(HTTP_PREFIX.length());
		}
		return url;
	}

	public void setMatch(Match match)
	{
		this.match = match;
	}

	@Override
	public String toString()
	{
		final StringBuilder builder = new StringBuilder();
		builder.append("Action ");
		builder.append(name);
		builder.append(" (");
		boolean comma = false;
		for (String code : codes)
		{
			if (comma)
			{
				if (match == Match.all)
				{
					builder.append(" + ");
				}
				else if (match == Match.any)
				{
					builder.append(", ");
				}
				else if (match == Match.sequence)
				{
					builder.append(" -> ");
				}
			}
			else
			{
				comma = true;
			}

			builder.append(code);
		}
		builder.append(")");
		return builder.toString();
	}

	public Integer getFramesRequired()
	{
		return framesRequired;
	}

	public void setFramesRequired(Integer framesRequired)
	{
		this.framesRequired = framesRequired;
	}

	public Integer getFramesAwarded()
	{
		return framesAwarded;
	}

	public void setFramesAwarded(Integer framesAwarded)
	{
		this.framesAwarded = framesAwarded;
	}

	public Double getMinimumSize()
	{
		return minimumSize;
	}

	public void setMinimumSize(Double minimumSize)
	{
		this.minimumSize = minimumSize;
	}

	public ChecksumOption getChecksumOption()
	{
		return checksumOption;
	}

	public void setChecksumOption(ChecksumOption checksumOption)
	{
		this.checksumOption = checksumOption;
	}

} public class Experience
{

	public enum Status
	{
		loaded, modified, saving
	}

	private final List<Action> actions = new ArrayList<>();
	private final List<Availability> availabilities = new ArrayList<>();
	private final List<String> pipeline = new ArrayList<>();
	private String id;
	private String name;
	private String icon;
	private String image;
	private String description;
	private String author;
	private String callback;
	private String originalID;
	private String requestedAutoFocusMode;

	private Boolean canCopy;
	private Boolean scanHistoryEnabled;

	private Boolean openWithoutUserInput;

	private String backgroundColor;
	private String foregroundColor;
	private String highlightBackgroundColor;
	private String highlightForegroundColor;
	private String scanScreenTextTitle;
	private String scanScreenTextDesciption;

	private Boolean fullscreen;

	public Experience()
	{
	}

	@NonNull
	public List<Action> getActions()
	{
		return actions;
	}

	@NonNull
	public List<Availability> getAvailabilities()
	{
		return availabilities;
	}

	public String getCallback()
	{
		return callback;
	}

	public void setCallback(String callback)
	{
		this.callback = callback;
	}

	public String getDescription()
	{
		return description;
	}

	public void setDescription(String description)
	{
		this.description = description;
	}

	public List<String> getPipeline()
	{
		return pipeline;
	}

	public String getIcon()
	{
		return icon;
	}

	public void setIcon(String icon)
	{
		this.icon = icon;
		//notifyPropertyChanged(BR.icon);
	}

	public String getId()
	{
		return id;
	}

	public void setId(String id)
	{
		this.id = id;
	}

	public String getImage()
	{
		return image;
	}

	public void setImage(String image)
	{
		this.image = image;
		//notifyPropertyChanged(BR.image);
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public String getOriginalID()
	{
		return originalID;
	}

	public void setOriginalID(String originalID)
	{
		this.originalID = originalID;
	}

	public boolean isSharable()
	{
		return id != null && (id.startsWith("http:") || id.startsWith("https:"));
	}

	private boolean hasCode(String code, Action.Match... matches)
	{
		for (Action action : actions)
		{
			for (Action.Match match : matches)
			{
				if (match == action.getMatch())
				{
					if (action.getCodes().contains(code))
					{
						return true;
					}
				}
			}
		}
		return false;
	}

	public boolean equals(Experience other)
	{
		return (this.getId()!=null && this.getId().equals(other.getId())) || this==other;
	}

	@Override
	public int hashCode()
	{
		return this.getId()!=null ? this.getId().hashCode() : 0;
	}

	public Action getActionForCode(String code)
	{
		if (code != null)
		{
			for (Action action : this.getActions())
			{
				for (String codeFromAction : action.getCodes())
				{
					if (code.equals(codeFromAction))
					{
						return action;
					}
				}
			}
		}
		return null;
	}

	public String getRequestedAutoFocusMode()
	{
		return requestedAutoFocusMode;
	}

	public void setRequestedAutoFocusMode(String requestedAutoFocusMode)
	{
		this.requestedAutoFocusMode = requestedAutoFocusMode;
	}

	public Boolean getCanCopy()
	{
		return canCopy;
	}

	public void setCanCopy(Boolean canCopy)
	{
		this.canCopy = canCopy;
	}

	public Boolean getScanHistoryEnabled()
	{
		return scanHistoryEnabled;
	}

	public void setScanHistoryEnabled(Boolean scanHistoryEnabled)
	{
		this.scanHistoryEnabled = scanHistoryEnabled;
	}

	public String getBackgroundColor()
	{
		return backgroundColor;
	}

	public void setBackgroundColor(String backgroundColor)
	{
		this.backgroundColor = backgroundColor;
	}

	public String getForegroundColor()
	{
		return foregroundColor;
	}

	public void setForegroundColor(String forgroundColor)
	{
		this.foregroundColor = forgroundColor;
	}
	public String getHighlightForegroundColor()
	{
		return highlightForegroundColor;
	}

	public void setHighlightForegroundColor(String highlighForgroundColor)
	{
		this.highlightForegroundColor = highlighForgroundColor;
	}

	public String getHighlightBackgroundColor()
	{
		return highlightBackgroundColor;
	}

	public void setHighlightBackgroundColor(String highlighBackgroundColor)
	{
		this.highlightBackgroundColor = highlighBackgroundColor;
	}

	public Boolean getOpenWithoutUserInput()
	{
		return openWithoutUserInput;
	}

	public void setOpenWithoutUserInput(Boolean openWithoutUserInput)
	{
		this.openWithoutUserInput = openWithoutUserInput;
	}
	public String getScanScreenTextTitle()
	{
		return scanScreenTextTitle;
	}

	public void setScanScreenTextTitle(String scanScreenTextTitle)
	{
		this.scanScreenTextTitle = scanScreenTextTitle;
	}

	public String getScanScreenTextDesciption()
	{
		return scanScreenTextDesciption;
	}

	public void setScanScreenTextDesciption(String scanScreenTextDesciption)
	{
		this.scanScreenTextDesciption = scanScreenTextDesciption;
	}


	public Boolean getFullscreen()
	{
		return fullscreen;
	}

	public void setFullscreen(Boolean fullscreen)
	{
		this.fullscreen = fullscreen;
	}
} public class MarkerImage
{
	public final String code;
	public final Bitmap image;
	public final float x, y, width, height;
	public boolean detectionActive = true;
	public boolean newDetection = true;

	public MarkerImage(String code, Bitmap image, float x, float y, float width, float height)
	{
		this.code = code;
		this.image = image;
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
	}
} public class ScannerActivity extends AppCompatActivity
{
	private static final int CAMERA_PERMISSION_REQUEST = 47;
	private LinearLayout settingIcons;
	protected ProgressBar progressBar;
	private ArtcodeDetector detector;
	private Experience experience;
	private VisibilityAnimator menuAnimator;
	private TextAnimator textAnimator;
	protected CameraView cameraView;

	@SuppressWarnings("UnusedParameters")
	public void hideMenu(View view)
	{
		menuAnimator.hideView();
	}

	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
	private void hideSystemUI()
	{
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
		{
			getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
		}
	}

	protected View actionView;
	protected VisibilityAnimator actionAnimator;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

		hideSystemUI();

		setContentView(R.layout.scanner);

		settingIcons = findViewById(R.id.settingsSwitches);
		progressBar = findViewById(R.id.progressBar);
		cameraView = findViewById(R.id.cameraView);

		Toolbar toolbar = findViewById(R.id.toolbar);
		setSupportActionBar(toolbar);
		if (getSupportActionBar() != null)
		{
			getSupportActionBar().setDisplayShowTitleEnabled(false);
		}

		if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)
		{
			ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST);
		}

		menuAnimator = new VisibilityAnimator(findViewById(R.id.settingsMenu), findViewById(R.id.settingsMenuButton));
		TextView settingsFeedback = findViewById(R.id.settingsFeedback);
		textAnimator = new TextAnimator(settingsFeedback);


		ViewGroup bottomView = findViewById(R.id.bottomView);
		if (bottomView != null)
		{
			actionView = getLayoutInflater().inflate(R.layout.scanner_action, bottomView, false);
			bottomView.addView(actionView);
			actionAnimator = new VisibilityAnimator(actionView);
		}

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
		{
			progressBar.setIndeterminateTintList(ColorStateList.valueOf(0xff33b5e5));
		}
	}

	@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
	{
		if (requestCode == CAMERA_PERMISSION_REQUEST)
		{// If request is cancelled, the result arrays are empty.
			if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
			{
				startScanning();
			}
			else
			{
				Log.i("a", "Permission not granted");
				// TODO
			}
		}
	}

	public void loaded(Experience experience)
	{
		this.experience = experience;
		setExperienceStyle();
		startScanning();
	}

	/**
	 * This function sets the colors of the scan screen (only if the experience contains colors).
	 */
	public void setExperienceStyle()
	{
		if (this.experience != null)
		{
			if (this.experience.getBackgroundColor() != null || this.experience.getForegroundColor() != null)
			{

				if (this.experience.getBackgroundColor() != null)
				{
					View topFrame = findViewById(R.id.topView);
					View bottomFrame = findViewById(R.id.bottomView);
					int backgroundColor = Color.parseColor(this.experience.getBackgroundColor());
					// If no transparency is set add default transparency to background:
					if (this.experience.getBackgroundColor().length() <= 7)
						backgroundColor &= 0xbbffffff;
					if (topFrame != null) topFrame.setBackgroundColor(backgroundColor);
					if (bottomFrame != null) bottomFrame.setBackgroundColor(backgroundColor);
				}

				if (this.experience.getForegroundColor() != null)
				{
					int foregroundColor = Color.parseColor(this.experience.getForegroundColor());
					Toolbar v = findViewById(R.id.toolbar);
					if (v != null)
					{
						v.setTitleTextColor(foregroundColor);
						// set back icon color:
						try
						{
							if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
							{
								v.getNavigationIcon().setTint(foregroundColor);
							}
							else
							{
								v.getNavigationIcon().setColorFilter(new
										PorterDuffColorFilter(foregroundColor, PorterDuff.Mode.MULTIPLY));
							}
						}
						catch (NullPointerException e)
						{
							Log.w("", "Exception setting toolbar icon colour.", e);
						}
					}


					TextView scanScreenTextTitle = findViewById(R.id.scanScreenTextTitle);
					if (scanScreenTextTitle != null)
					{
						scanScreenTextTitle.setTextColor(foregroundColor);
					}
					TextView scanScreenTextDesc = findViewById(R.id.scanScreenTextDesc);
					if (scanScreenTextDesc != null)
					{
						scanScreenTextDesc.setTextColor(foregroundColor);
					}
				}

			}

			if (this.experience.getHighlightBackgroundColor() != null && this.experience.getHighlightForegroundColor() != null)
			{
				int foregroundColor = Color.parseColor(this.experience.getHighlightForegroundColor());
				int backgroundColor = Color.parseColor(this.experience.getHighlightBackgroundColor());

				Button b = (Button) findViewById(R.id.scan_event_button);
				if (b != null)
				{
					b.setTextColor(foregroundColor);
					b.setBackgroundColor(backgroundColor);
				}
				ProgressBar pb = findViewById(R.id.progressBar);
				if (pb != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
				{
					pb.setIndeterminateTintList(ColorStateList.valueOf(backgroundColor));
				}
			}

			if (this.experience.getScanScreenTextTitle() != null)
			{
				TextView textView = findViewById(R.id.scanScreenTextTitle);
				if (textView != null)
				{
					textView.setVisibility(View.VISIBLE);
					textView.setText(this.experience.getScanScreenTextTitle());
				}
			}
			if (this.experience.getScanScreenTextDesciption() != null)
			{
				TextView textView = findViewById(R.id.scanScreenTextDesc);
				if (textView != null)
				{
					textView.setVisibility(View.VISIBLE);
					textView.setText(this.experience.getScanScreenTextDesciption());
				}
			}

		}
	}

	@SuppressWarnings("UnusedParameters")
	public void showMenu(View view)
	{
		createSettingsUI(detector.getSettings());
		menuAnimator.showView();
	}

	protected Experience getExperience()
	{
		return experience;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item)
	{
		if (item.getItemId() == android.R.id.home)
		{
			if (getCallingActivity() != null)
			{
				setResult(RESULT_CANCELED);
				finish();
				return true;
			}
		}
		return super.onOptionsItemSelected(item);
	}

	private void startScanning()
	{
		if (experience != null)
		{
			Log.i("a", "Start Scanning");
			detector = this.getNewDetector(experience);
			detector.setCallback(new DetectorCallback()
			{
				@Override
				public void detectionStart(final int margin)
				{
					runOnUiThread(new Runnable()
					{
						@Override
						public void run()
						{
							Log.i("cameraErrorView", "Getting camera error view...");
							View errorView = findViewById(R.id.cameraError);
							if (errorView != null)
							{
								Log.i("cameraErrorView", "Setting camera error view to GONE");
								errorView.setVisibility(View.GONE);
							}
							progressBar.setVisibility(View.VISIBLE);
							if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
							{
								View topView = findViewById(R.id.topView);
								if (topView != null)
								{
									topView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, margin));
								}
								View bottomView = findViewById(R.id.bottomView);
								if (bottomView != null)
								{
									bottomView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, margin));
								}
							}
							else
							{
								View topView = findViewById(R.id.topView);
								if (topView != null)
								{
									topView.setLayoutParams(new LinearLayout.LayoutParams(margin, ViewGroup.LayoutParams.MATCH_PARENT));
								}
								View bottomView = findViewById(R.id.bottomView);
								if (bottomView != null)
								{
									bottomView.setLayoutParams(new LinearLayout.LayoutParams(margin, ViewGroup.LayoutParams.MATCH_PARENT));
								}
							}
						}
					});
				}
			});
			detector.setOverlay((ImageView) findViewById(R.id.overlay));
			if (getSupportActionBar() != null && experience.getScanScreenTextTitle() == null)
			{
				getSupportActionBar().setDisplayShowTitleEnabled(true);
				getSupportActionBar().setTitle(experience.getName());
			}
			cameraView.setDetector(detector, experience);

			if (cameraView.deviceNeedsTapToFocus())
			{
				// tap to focus
				View v = findViewById(R.id.thumbnailImageLayout);
				if (v != null)
				{
					v.setOnClickListener(new View.OnClickListener()
					{
						@Override
						public void onClick(View view)
						{
							setFocusTextVisible(false);
							cameraView.focus(new Runnable()
							{
								@Override
								public void run()
								{
									setFocusTextVisible(true);
								}
							});

						}
					});
				}
				setFocusTextVisible(true);
			}
		}
		else
		{
			cameraView.setDetector(null, null);
		}
	}

	private boolean focusTextAlreadyShown = false;

	private void setFocusTextVisible(final boolean visible)
	{
		final TextView focusTextView = findViewById(R.id.focusText);
		runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				if (focusTextView != null)
				{
					if (focusTextAlreadyShown)
					{
						focusTextView.setText(R.string.tap_to_refocus);
						focusTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
						focusTextView.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
					}
					if (visible)
					{
						focusTextView.setVisibility(View.VISIBLE);
					}
					else
					{
						focusTextView.setVisibility(View.INVISIBLE);
						focusTextAlreadyShown = true;
					}
				}
			}
		});
	}

	private String lastFoundCode = null;

	private void onCodeDetected(final String markerCode)
	{
		if (experience != null)
		{
			if (experience.getOpenWithoutUserInput() == null || experience.getOpenWithoutUserInput())
			{
				returnCode(markerCode);
			}
			else if (!markerCode.equals(lastFoundCode))
			{
				final Experience experience = getExperience();
				final Action action = experience.getActionForCode(markerCode);
				if (action != null)
				{
					lastFoundCode = markerCode;
					final Button actionButton = actionView.findViewById(R.id.scan_event_button);
					if (actionButton != null)
					{

						runOnUiThread(new Runnable()
						{
							@Override
							public void run()
							{
								actionButton.setText(action.getName() != null && !action.getName().equals("") ? action.getName() : (action.getDisplayUrl() != null && !action.getDisplayUrl().equals("") ? action.getDisplayUrl() : markerCode));
								actionButton.setOnClickListener(new View.OnClickListener()
								{
									@Override
									public void onClick(View v)
									{
										returnCode(markerCode);
									}
								});
							}
						});
						runOnUiThread(new Runnable()
						{
							@Override
							public void run()
							{
								actionAnimator.showView();
								progressBar.setVisibility(View.INVISIBLE);
							}
						});
					}
				}
			}
		}
	}

	private void returnCode(String markerCode)
	{
		Log.i("Marker", "MarkerDisplay Detected: " + markerCode);
		if (markerCode != null)
		{
			if (experience.getCallback() != null)
			{
				startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(experience.getCallback().replace("{code}", markerCode))));
			}
			else
			{
				Intent intent = getIntent();
				intent.putExtra("marker", markerCode);
				setResult(RESULT_OK, intent);
				finish();
			}
		}
	}

	protected void loadExperience(Bundle savedInstanceState)
	{
		if (savedInstanceState != null && savedInstanceState.containsKey("experience"))
		{

			loaded(new Gson().fromJson(savedInstanceState.getString("experience"), Experience.class));
		}
		else
		{
			Intent intent = getIntent();
			if (intent.hasExtra("experience"))
			{
				loaded(new Gson().fromJson(intent.getStringExtra("experience"), Experience.class));
			}
		}
	}

	@Override
	protected void onPostCreate(Bundle savedInstanceState)
	{
		super.onPostCreate(savedInstanceState);
		loadExperience(savedInstanceState);
	}

	private Drawable getTintedDrawable(@DrawableRes int drawable, @ColorInt int color)
	{
		final Drawable original = ContextCompat.getDrawable(this, drawable);
		if (original != null)
		{
			final Drawable wrapDrawable = DrawableCompat.wrap(original);
			DrawableCompat.setTint(wrapDrawable, color);
			return wrapDrawable;
		}
		return null;
	}

	private void createSettingsUI(List<DetectorSetting> settings)
	{
		settingIcons.removeAllViews();
		if (settings != null && !settings.isEmpty())
		{
			final int padding = getResources().getDimensionPixelSize(R.dimen.setting_padding);
			List<DetectorSetting> settingList = settings;
			if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
			{
				settingList = Lists.reverse(settings);
			}
			for (final DetectorSetting setting : settingList)
			{
				final ImageView button = new ImageView(this);
				button.setContentDescription(getString(setting.getText()));
				final int[] attrs = new int[]{android.R.attr.selectableItemBackground};
				final TypedArray ta = obtainStyledAttributes(attrs);
				final Drawable drawableFromTheme = ta.getDrawable(0);
				ta.recycle();
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
				{
					button.setImageResource(setting.getIcon());
					button.setImageTintList(ColorStateList.valueOf(Color.WHITE));
					button.setBackground(drawableFromTheme);
				}
				else
				{
					button.setImageDrawable(getTintedDrawable(setting.getIcon(), Color.WHITE));
					button.setBackgroundDrawable(drawableFromTheme);
				}

				button.setOnClickListener(new View.OnClickListener()
				{
					@Override
					public void onClick(View v)
					{
						setting.nextValue();
						if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
						{
							button.setImageResource(setting.getIcon());
						}
						else
						{
							button.setImageDrawable(getTintedDrawable(setting.getIcon(), Color.WHITE));
						}
						button.setContentDescription(getString(setting.getText()));
						textAnimator.setText(setting.getText());
					}
				});
				final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1);
				params.weight = 1;
				button.setLayoutParams(params);
				settingIcons.addView(button);
				button.setPadding(padding, padding, padding, padding);
			}

		}

		menuAnimator.setViewVisible(settingIcons.getChildCount() > 0);
	}

	protected ArtcodeDetector getNewDetector(Experience experience)
	{
		return new ArtcodeDetector(this, experience, new MarkerCodeDetectionHandler(this.experience, new CodeDetectionHandler()
		{
			@Override
			public void onMarkerCodeDetected(String code)
			{
				onCodeDetected(code);
			}
		}), this.cameraView);
	}
} public interface ArtcodeServer
{
	void add(Account account);

	Account getAccount(String id);

	Account createAccount(String id);

	List<Account> getAccounts();

	void saveRecent(List<String> recent);

	void saveStarred(List<String> starred);

	void loadExperience(String id, LoadCallback<Experience> callback);

	void loadRecent(LoadCallback<List<String>> callback);

	void loadRecommended(LoadCallback<Map<String, List<String>>> callback, Location location);

	void loadStarred(LoadCallback<List<String>> callback);

	void logScan(String uri, Action action);

	List<ScanEvent> getScanHistory(String id);

	void search(String query, LoadCallback<List<String>> callback);
} public interface LoadCallback<T>
{
	void loaded(T item);
	void error(Throwable e);
}
 public class MarkerHistoryViewController
{

    private static final int SEPARATOR_WIDTH_DP = 25;
    private static final int VIEW_WIDTH_DP = 50;
    private static final int IMAGE_WIDTH_DP = 45;
    private static final int BOTTOM_MARGIN_DP = 5;
    private static final int ANIMATION_DURATION_MS = 300;

    private final Context context;
    private final RelativeLayout relativeLayout;
    private Handler uiHandler;
    private List<MarkerImage> existingMarkerImages = new ArrayList<>();
    private Action existingAction = null;

    private final float displayDensity;

    public MarkerHistoryViewController(Context context, RelativeLayout relativeLayout, Handler uiHandler)
    {
        this.context = context;
        this.relativeLayout = relativeLayout;
        this.uiHandler = uiHandler;

        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        displayDensity = displayMetrics.density;
    }

    private Map<MarkerImage, View> displayedViews = new HashMap<>();
    private Map<Integer, View> missingViewsByPosition = new HashMap<>();
    private Map<Integer, View> seperatorViewsByPosition = new HashMap<>();

    public void update(List<MarkerImage> incomingMarkerImages, final Action currentOrFutureAction)
    {
        if (currentOrFutureAction == null || incomingMarkerImages == null || incomingMarkerImages.isEmpty())
        {
            this.existingMarkerImages.clear();
            this.existingAction = null;
            incomingMarkerImages = new ArrayList<>();
        }

        final List<MarkerImage> markerImages = incomingMarkerImages;
        final Action.Match existingMatchType = existingAction!=null ? existingAction.getMatch() : Action.Match.any;
        final Action.Match currentMatchType = currentOrFutureAction!=null ? currentOrFutureAction.getMatch() : Action.Match.any;

        final int n = markerImages.size();
        final int viewSizePX = (int) (VIEW_WIDTH_DP * displayDensity);
        final int separatorWidthPX= (int) (SEPARATOR_WIDTH_DP *displayDensity);
        final int startX = (int) (relativeLayout.getWidth() / 2 - (n * viewSizePX / 2f) - ((n-1) * separatorWidthPX / 2f));
        final int finalTranslationY = relativeLayout.getHeight() - viewSizePX - (int) (BOTTOM_MARGIN_DP * displayDensity);
        final int finalTranslationYForSeparator = finalTranslationY + (int)(12.5*displayDensity);

        this.uiHandler.post(new Runnable()
        {
            @Override
            public void run()
            {

                // animate removal of views not in markerImages (and unneeded separators/placeholders)
                // TODO: instead of creating/destroying separators/placeholders they should be reused.

                // Remove placeholders
                List<Map.Entry<Integer, View>> missingToRemove = new ArrayList<>();
                for (Map.Entry<Integer, View> entry : missingViewsByPosition.entrySet())
                {
                    if (entry.getKey() >= markerImages.size()-1)
                    {
                        missingToRemove.add(entry);
                    }
                }
                for (Map.Entry<Integer, View> entry : missingToRemove)
                {
                    final View view = entry.getValue();
                    animateRemoval(view);
                    missingViewsByPosition.remove(entry.getKey());
                }

                // Remove separators
                List<Map.Entry<Integer, View>> separatorsToRemove = new ArrayList<>();
                if (currentOrFutureAction!=null && existingMatchType==currentMatchType)
                {
                    for (Map.Entry<Integer, View> entry : seperatorViewsByPosition.entrySet())
                    {
                        if (entry.getKey() >= markerImages.size() - 2)
                        {
                            separatorsToRemove.add(entry);
                        }
                    }
                }
                else
                {
                    separatorsToRemove.addAll(seperatorViewsByPosition.entrySet());
                }
                for (Map.Entry<Integer, View> entry : separatorsToRemove)
                {
                    final View view = entry.getValue();
                    animateRemoval(view);
                    seperatorViewsByPosition.remove(entry.getKey());
                }

                // Remove marker images
                List<Map.Entry<MarkerImage, View>> toRemove = new ArrayList<>();
                for (Map.Entry<MarkerImage, View> entry : displayedViews.entrySet())
                {
                    if (!markerImages.contains(entry.getKey()))
                    {
                        toRemove.add(entry);
                    }
                }
                for (Map.Entry<MarkerImage, View> entry : toRemove)
                {
                    final View view = entry.getValue();
                    animateRemoval(view);
                    displayedViews.remove(entry.getKey());
                }

                // Add or re-position views of images in markerImages (and separators/placeholders)

                int count = 0;
                for (MarkerImage markerImage : markerImages)
                {

                    if (markerImage != null)
                    {
                        // add/re-position marker image

                        final View missingView = missingViewsByPosition.get(count);
                        if (missingView != null)
                        {
                            animateRemoval(missingView);
                            missingViewsByPosition.remove(count);
                        }

                        View markerImageView = displayedViews.get(markerImage);
                        if (markerImageView == null)
                        {
                            markerImageView = LayoutInflater.from(context).inflate(R.layout.marker_thumbnail, relativeLayout);
                            final ImageView imageView = markerImageView.findViewById(R.id.marker_thumbnail_image);
                            if (imageView == null)
                            {
                                continue;
                            }
                            imageView.setImageBitmap(markerImage.image);
                        }

                        // Note: Translation ignores scale, and scale scales around the centre of the view.

                        int finalTranslationX = startX + count * (viewSizePX + separatorWidthPX);

                        if (markerImage.newDetection)
                        {
                            final int initialImageWidthPX = (int) (relativeLayout.getWidth() * markerImage.width);
                            final int initialImageHeightPX = (int) (relativeLayout.getHeight() * markerImage.height);

                            final float initialScale = (Math.max(initialImageWidthPX, initialImageHeightPX) / displayDensity) / IMAGE_WIDTH_DP;

                            final int initialImageXCenter = (int) (relativeLayout.getWidth() * markerImage.x + initialImageWidthPX / 2);
                            final int initialImageYCenter = (int) (relativeLayout.getHeight() * markerImage.y + initialImageHeightPX / 2);
                            final int initialViewX = initialImageXCenter - viewSizePX / 2;
                            final int initialViewY = initialImageYCenter - viewSizePX / 2;

                            markerImageView.setTranslationX(initialViewX);
                            markerImageView.setTranslationY(initialViewY);
                            markerImageView.setScaleX(initialScale);
                            markerImageView.setScaleY(initialScale);
                            markerImageView.setAlpha(0f);

                            if (!displayedViews.containsKey(markerImage))
                            {
                                relativeLayout.addView(markerImageView);
                                displayedViews.put(markerImage, markerImageView);
                            }
                            animateEnterOrMove(markerImageView, finalTranslationX, finalTranslationY, 1);
                        }
                        else
                        {
                            if (!displayedViews.containsKey(markerImage))
                            {
                                markerImageView.setScaleX(1);
                                markerImageView.setScaleY(1);
                                markerImageView.setTranslationX(finalTranslationX);
                                markerImageView.setTranslationY(finalTranslationY);
                                markerImageView.setAlpha(0f);
                                relativeLayout.addView(markerImageView);
                                displayedViews.put(markerImage, markerImageView);
                            }
                            animateEnterOrMove(markerImageView, finalTranslationX, finalTranslationY, 1);
                        }
                    }
                    else
                    {
                        // add/move placeholder for missing marker

                        View view = missingViewsByPosition.get(count);

                        final int finalTranslationX = startX + count * (viewSizePX + (int) (SEPARATOR_WIDTH_DP * displayDensity));

                        if (view == null)
                        {
                            view = createMarkerPlaceholder();
                            view.setTranslationX(finalTranslationX);
                            view.setTranslationY(finalTranslationY);
                            view.setAlpha(0f);
                            missingViewsByPosition.put(count, view);
                            relativeLayout.addView(view);
                        }

                        animateEnterOrMove(view, finalTranslationX, finalTranslationY);
                    }

                    // add/move separator
                    if (count < markerImages.size()-1)
                    {
                        View view = seperatorViewsByPosition.get(count);

                        final int finalTranslationX = startX + (count+1) * (viewSizePX + (int) (SEPARATOR_WIDTH_DP * displayDensity)) - (int)(SEPARATOR_WIDTH_DP *displayDensity);

                        if (view == null)
                        {
                            if (currentMatchType == Action.Match.all)
                            {
                                view = createGroupSeparator();
                            }
                            else
                            {
                                view = createSequenceSeparator();
                            }
                            view.setTranslationX(finalTranslationX);
                            view.setTranslationY(finalTranslationYForSeparator);
                            view.setAlpha(0f);
                            seperatorViewsByPosition.put(count, view);
                            relativeLayout.addView(view);
                        }

                        animateEnterOrMove(view, finalTranslationX, finalTranslationYForSeparator);
                    }

                    ++count;
                }
            }
        });


        this.existingMarkerImages = markerImages;
        this.existingAction = currentOrFutureAction;
    }

    private void animateEnterOrMove(View view, int x, int y)
    {
        view.animate()
                .alpha(1)
                .translationX(x)
                .translationY(y)
                .setDuration(ANIMATION_DURATION_MS)
                .setInterpolator(new LinearInterpolator())
                .start();
    }

    private void animateEnterOrMove(View view, int x, int y, float scale)
    {
        view.animate()
                .alpha(1)
                .translationX(x)
                .translationY(y)
                .scaleX(scale)
                .scaleY(scale)
                .setDuration(ANIMATION_DURATION_MS)
                .setInterpolator(
                        new LinearInterpolator()
                ).start();
    }

    private void animateRemoval(final View view)
    {
        view.animate()
                .setListener(new Animator.AnimatorListener()
                {
                    @Override
                    public void onAnimationStart(Animator animator)
                    {

                    }

                    @Override
                    public void onAnimationEnd(Animator animator)
                    {
                        relativeLayout.removeView(view);
                    }

                    @Override
                    public void onAnimationCancel(Animator animator)
                    {

                    }

                    @Override
                    public void onAnimationRepeat(Animator animator)
                    {

                    }
                })
                .alpha(0)
                .setInterpolator(new LinearInterpolator())
                .setDuration(ANIMATION_DURATION_MS)
                .start();
    }

    private View createMarkerPlaceholder()
    {
        ImageView iv = new ImageView(context);
        Drawable d = ContextCompat.getDrawable(context, R.drawable.missing_marker_thumbnail_background);
        iv.setImageDrawable(d);
        return iv;
    }

    private View createGroupSeparator()
    {
        ImageView iv = new ImageView(context);
        Drawable d = ContextCompat.getDrawable(context, R.drawable.marker_thumbnail_separator_group_image);
        iv.setImageDrawable(d);
        return iv;
    }

    private View createSequenceSeparator()
    {
        ImageView iv = new ImageView(context);
        Drawable d = ContextCompat.getDrawable(context, R.drawable.marker_thumbnail_separator_sequence_image);
        iv.setImageDrawable(d);
        return iv;
    }

}
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.app.NavUtils;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.ContextCompat;
import com.google.gson.Gson;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import uk.ac.horizon.artcodes.Analytics;
import uk.ac.horizon.artcodes.Artcodes;
import uk.ac.horizon.artcodes.Features;
import uk.ac.horizon.artcodes.Hash;
import uk.ac.horizon.artcodes.R;
import uk.ac.horizon.artcodes.ScannerFeatures;
import uk.ac.horizon.artcodes.detect.ArtcodeDetector;
import uk.ac.horizon.artcodes.detect.handler.ActionDetectionHandler;
import uk.ac.horizon.artcodes.detect.handler.MarkerActionDetectionHandler;
import uk.ac.horizon.artcodes.detect.handler.MultipleMarkerActionDetectionHandler;
import uk.ac.horizon.artcodes.drawer.MarkerThumbnailDrawer;
import uk.ac.horizon.artcodes.model.Action;
import uk.ac.horizon.artcodes.model.Experience;
import uk.ac.horizon.artcodes.model.MarkerImage;
import uk.ac.horizon.artcodes.scanner.ScannerActivity;
import uk.ac.horizon.artcodes.server.ArtcodeServer;
import uk.ac.horizon.artcodes.server.LoadCallback;
import uk.ac.horizon.artcodes.ui.MarkerHistoryViewController;
/*
 * Artcodes recognises a different marker scheme that allows the
 * creation of aesthetically pleasing, even beautiful, codes.
 * Copyright (C) 2013-2016  The University of Nottingham
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Affero General Public License as published
 *     by the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Affero General Public License for more details.
 *
 *     You should have received a copy of the GNU Affero General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.ac.horizon.artcodes.activity;






public class ArtcodeActivity extends ScannerActivity implements LoadCallback<Experience> {
	public static void start(Context context, Experience experience) {
		StaticActivityMessage.experience = experience;
		TaskStackBuilder.create(context)
				.addNextIntent(new Intent(context, NavigationActivity.class))
				.addNextIntent(new Intent(context, ExperienceActivity.class))
				.addNextIntent(new Intent(context, ArtcodeActivity.class))
				.startActivities();
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		if (getSupportActionBar() != null) {
			getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		}
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if (item.getItemId() == android.R.id.home) {
			final Experience experience = getExperience();
			NavUtils.navigateUpTo(this, ExperienceActivity.intent(this, experience));
			return true;
		}
		return super.onOptionsItemSelected(item);
	}

	@Override
	public void loaded(final Experience experience) {
		super.loaded(experience);

		if (experience != null) {
			getServer().loadRecent(new LoadCallback<List<String>>() {
				@Override
				public void loaded(List<String> item) {
					item.remove(experience.getId());
					item.add(0, experience.getId());
					getServer().saveRecent(item);
				}

				@Override
				public void error(Throwable e) {
					Analytics.trackException(e);
				}
			});
		}
	}

	@Override
	public void error(Throwable e) {
		Analytics.trackException(e);
	}

	@Override
	protected void loadExperience(Bundle savedInstanceState) {
		if (savedInstanceState != null && savedInstanceState.containsKey("experience")) {

			loaded(new Gson().fromJson(savedInstanceState.getString("experience"), Experience.class));
		} else {
			Intent intent = getIntent();
			if (intent.hasExtra("experience")) {
				loaded(new Gson().fromJson(intent.getStringExtra("experience"), Experience.class));
			} else {
				final Uri data = intent.getData();
				if (data != null) {
					getServer().loadExperience(data.toString(), this);
				} else {

					if (StaticActivityMessage.experience != null) {
						loaded(StaticActivityMessage.experience);
					}
				}
			}
		}
	}

	private Artcodes getArtcodes() {
		return (Artcodes) getApplication();
	}

	private ArtcodeServer getServer() {
		return getArtcodes().getServer();
	}

	private void onActionChanged(final Action action) {
		Log.i("action", "" + action);
		if (action != null) {

			if ((getExperience().getOpenWithoutUserInput() != null && getExperience().getOpenWithoutUserInput()) ||
					Features.open_without_touch.isEnabled(getApplicationContext())) {
				if (action.getUrl() != null) {
					final Experience experience = getExperience();
					Analytics.logOpenScan(experience.getId(), action);
					CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
					// TODO Warmup urls
					//builder.setSession(session);
					builder.setToolbarColor(ContextCompat.getColor(ArtcodeActivity.this, R.color.apptheme_primary));
					CustomTabsIntent customTabsIntent = builder.build();
					customTabsIntent.launchUrl(ArtcodeActivity.this, Uri.parse(processURL(action.getUrl(), action)));
				}
			} else {
				final Experience experience = getExperience();
				getServer().logScan(experience.getId(), action);
				Analytics.logScan(experience.getId(), action);

				final Button actionButton = actionView.findViewById(R.id.scan_event_button);
				if (actionButton != null) {
					runOnUiThread(() -> {
						actionButton.setText(action.getName() != null && !action.getName().equals("") ? action.getName() : (action.getDisplayUrl() != null && !action.getDisplayUrl().equals("") ? action.getDisplayUrl() : action.getCodes().get(0)));
						actionButton.setOnClickListener(v -> {
							if (action.getUrl() != null) {
								Analytics.logOpenScan(experience.getId(), action);
								CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
								// TODO Warmup urls
								//builder.setSession(session);
								builder.setToolbarColor(ContextCompat.getColor(ArtcodeActivity.this, R.color.apptheme_primary));
								CustomTabsIntent customTabsIntent = builder.build();
								customTabsIntent.launchUrl(ArtcodeActivity.this, Uri.parse(processURL(action.getUrl(), action)));
							}
						});
					});
				}
				runOnUiThread(() -> {
					actionAnimator.showView();
					progressBar.setVisibility(View.INVISIBLE);
				});
			}
		} else {
			runOnUiThread(() -> {
				actionAnimator.hideView();
				progressBar.setVisibility(View.VISIBLE);
			});
		}
	}

	@Override
	protected ArtcodeDetector getNewDetector(Experience experience) {
		if (ScannerFeatures.combined_markers.isEnabled(getApplicationContext())) {