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 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 interface Account
{
	void loadLibrary(LoadCallback<List<String>> callback);

	void saveExperience(Experience experience);
	void saveExperience(Experience experience, AccountProcessCallback saveCallback);

	void deleteExperience(Experience experience);
	void deleteExperience(Experience experience, AccountProcessCallback saveCallback);

	String getId();

	String getDisplayName();

	void setDisplayName(String displayName);

	boolean canEdit(String uri);

	boolean isSaving(String uri);

	boolean isLocal();

	boolean validates() throws UserRecoverableAuthException;

	boolean load(String uri, URILoaderCallback callback);

	interface AccountProcessCallback
	{
		/**
		 * @param success if the process was a success (e.g. file uploaded).
         * @param experience The created resource, can be null.
         */
		void accountProcessCallback(boolean success, Experience experience);
	}
}
 public class ExperienceLibraryFragment extends ArtcodeFragmentBase
{
	private ExperienceAdapter adapter;
	private Account account;

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

	@NonNull
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		ListBinding binding = ListBinding.inflate(inflater, container, false);
		adapter = new ExperienceSortedListAdapter(getActivity(), getServer());
		adapter.enableFABPadding();
		adapter.setEmptyIcon(R.drawable.ic_folder_144dp);
		adapter.setEmptyMessage(getString(R.string.empty));
		adapter.setEmptyDetail(getString(R.string.emptyHint));
		binding.setAdapter(adapter);

		final FloatingActionButton fab = new FloatingActionButton(getContext());
		fab.setImageDrawable(getTintedDrawable(R.drawable.ic_add_24dp, Color.WHITE));
		//fab.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.apptheme_accent));
		fab.setOnClickListener(v -> ExperienceEditActivity.start(getActivity(), new Experience(), account));
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
		{
			fab.setElevation(4);
		}

		final int spacing = getResources().getDimensionPixelSize(R.dimen.padding);
		final FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
		layoutParams.gravity = Gravity.BOTTOM | Gravity.END;
		layoutParams.setMargins(spacing, spacing, spacing, spacing);

		fab.setLayoutParams(layoutParams);

		((ViewGroup) binding.getRoot()).addView(fab);

		return binding.getRoot();
	}

	@Override
	public void onResume()
	{
		super.onResume();
		final Account account = getAccount();
		getActivity().setTitle(account.getDisplayName());
		adapter.loadStarted();
		new Thread(() -> {
			try
			{
				account.validates();

				account.loadLibrary(adapter);
			}
			catch (UserRecoverableAuthException e)
			{
				startActivityForResult(e.getIntent(), 1);
			}
		}).start();

	}

	private Account getAccount()
	{
		if (account == null)
		{
			if (getArguments() != null)
			{
				String accountID = getArguments().getString("account");
				if (accountID != null)
				{
					Account selected = getServer().getAccount(accountID);
					if (selected != null)
					{
						account = selected;
					}
				}
			}
		}
		return account;
	}
} public class ExperienceRecentFragment extends ArtcodeFragmentBase
{
	private ExperienceAdapter adapter;

	@NonNull
	@Override
	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		ListBinding binding = ListBinding.inflate(inflater, container, false);
		adapter = new ExperienceListAdapter(getActivity(),getServer());
		binding.setAdapter(adapter);
		return binding.getRoot();
	}

	@Override
	public void onResume()
	{
		super.onResume();
		getActivity().setTitle(R.string.nav_recent);
		adapter.loadStarted();
		getServer().loadRecent(adapter);
	}
} public class ExperienceRecommendFragment extends ArtcodeFragmentBase
{
	private static final int LOCATION_PERMISSION_REQUEST = 87;

	private ExperienceGroupAdapter adapter;

	@NonNull
	@Override
	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		ListBinding binding = ListBinding.inflate(inflater, container, false);
		adapter = new ExperienceGroupAdapter(getActivity(), getServer());
		binding.setAdapter(adapter);
		return binding.getRoot();
	}

	@Override
	public void onStart()
	{
		super.onStart();

		if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)
		{
			requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST);
		}
	}

	@Override
	public void onResume()
	{
		super.onResume();
		getActivity().setTitle(R.string.nav_home);

		loadExperiences();
	}

	@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
	{
		if (requestCode == LOCATION_PERMISSION_REQUEST)
		{
			loadExperiences();
		}
	}

	private void loadExperiences()
	{
		loadLocal();
		adapter.loadStarted();
		getServer().loadRecommended(adapter.getCallback(), getLocation());
	}

	private void navigateTo(Fragment fragment)
	{
		if(getActivity() instanceof NavigationActivity)
		{
			NavigationActivity activity = (NavigationActivity) getActivity();
			activity.navigate(fragment, true);
		}
	}

	private void loadLocal()
	{
		adapter.loadStarted();
		getServer().loadRecent(adapter.getCallback("recent", v -> navigateTo(new ExperienceRecentFragment())));
		adapter.loadStarted();
		getServer().loadStarred(adapter.getCallback("starred", v -> navigateTo(new ExperienceStarFragment())));
	}

	@Nullable
	private Location getLocation()
	{
		if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)
		{
			final LocationManager locationManager = (LocationManager) getContext().getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
			float accuracy = Float.MAX_VALUE;
			Location location = null;
			for (String provider : locationManager.getProviders(new Criteria(), true))
			{
				Location newLocation = locationManager.getLastKnownLocation(provider);
				if (newLocation != null)
				{
					if (newLocation.getAccuracy() < accuracy)
					{
						accuracy = newLocation.getAccuracy();
						location = newLocation;
					}
				}
			}

			return location;
		}
		else
		{
			Log.i("location", "No location permission");
		}
		return null;
	}
} public class ExperienceSearchFragment extends ArtcodeFragmentBase
{
	private ExperienceAdapter adapter;
	private String query;

	@NonNull
	@Override
	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		final ListBinding binding = ListBinding.inflate(inflater, container, false);
		adapter = new ExperienceListAdapter(getActivity(), getServer());
		adapter.setEmptyIcon(R.drawable.ic_search_144dp);
		binding.setAdapter(adapter);
		return binding.getRoot();
	}

	@Override
	public void onResume()
	{
		super.onResume();
		if (getArguments() != null)
		{
			String query = getArguments().getString("query");
			if (query != null)
			{
				search(query);
			}
		}
		//getActivity().setTitle(R.string.nav_recent);
	}

	public void search(String query)
	{
		if (query != null)
		{
			final String queryString = query.trim();
			if (!queryString.isEmpty() && (this.query == null || !this.query.equals(queryString)))
			{
				adapter.loadStarted();
				adapter.setEmptyMessage(getString(R.string.search_empty, queryString));
				getServer().search(queryString, adapter);
				this.query = queryString;
			}
		}
	}
} public class ExperienceStarFragment extends ArtcodeFragmentBase
{
	private ExperienceAdapter adapter;

	@NonNull
	@Override
	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		ListBinding binding = ListBinding.inflate(inflater, container, false);
		adapter = new ExperienceSortedListAdapter(getActivity(), getServer());
		binding.setAdapter(adapter);
		return binding.getRoot();
	}

	@Override
	public void onResume()
	{
		super.onResume();
		getActivity().setTitle(R.string.nav_starred);
		adapter.loadStarted();
		getServer().loadStarred(adapter);
	}
} public class FeatureListFragment extends Fragment {
	@Nullable
	@Override
	public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		ListBinding binding = ListBinding.inflate(inflater, container, false);
		final FeatureAdapter adapter = new FeatureAdapter(getActivity());
		binding.setAdapter(adapter);

		try {
			for(Features feature: Features.values()) {
				adapter.add(new FeatureItem(feature, getContext()));
			}
			for(ScannerFeatures feature: ScannerFeatures.values()) {
				adapter.add(new FeatureItem(feature, getContext()));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}


		return binding.getRoot();
	}
} public interface LoadCallback<T>
{
	void loaded(T item);
	void error(Throwable e);
}

import android.accounts.AccountManager;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.widget.BaseAdapter;
import android.widget.HeaderViewListAdapter;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.GravityCompat;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.auth.api.signin.GoogleSignInResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.material.navigation.NavigationView;
import java.util.List;
import uk.ac.horizon.artcodes.Analytics;
import uk.ac.horizon.artcodes.Features;
import uk.ac.horizon.artcodes.R;
import uk.ac.horizon.artcodes.account.Account;
import uk.ac.horizon.artcodes.databinding.NavigationBinding;
import uk.ac.horizon.artcodes.fragment.ExperienceLibraryFragment;
import uk.ac.horizon.artcodes.fragment.ExperienceRecentFragment;
import uk.ac.horizon.artcodes.fragment.ExperienceRecommendFragment;
import uk.ac.horizon.artcodes.fragment.ExperienceSearchFragment;
import uk.ac.horizon.artcodes.fragment.ExperienceStarFragment;
import uk.ac.horizon.artcodes.fragment.FeatureListFragment;
import uk.ac.horizon.artcodes.server.LoadCallback;
			}

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

	@Override
	protected void onNewIntent(Intent intent) {
		Log.i("a", "New intent " + intent);
		super.onNewIntent(intent);
	}

	public void navigate(Fragment fragment, boolean addToBackStack) {
		if (addToBackStack) {
			getSupportFragmentManager().beginTransaction()
					.replace(R.id.content, fragment, FRAGMENT_TAG)
					.addToBackStack(null)
					.commit();
		} else {
			getSupportFragmentManager().beginTransaction()
					.replace(R.id.content, fragment, FRAGMENT_TAG)
					.commit();
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		final MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.search_menu, menu);

		final MenuItem searchItem = menu.findItem(R.id.search);
		final SearchView searchView = (SearchView) searchItem.getActionView();
		final SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
		searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
		searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
			private Runnable delayedAction = null;

			@Override
			public boolean onQueryTextSubmit(String query) {
				search(query);
				return true;
			}

			@Override
			public boolean onQueryTextChange(final String newText) {
				if (delayedAction != null) {
					searchHandler.removeCallbacks(delayedAction);
					delayedAction = null;
				}

				if (newText.trim().length() > 3) {
					delayedAction = () -> search(newText);

					searchHandler.postDelayed(delayedAction, 1000);
				}
				return true;
			}
		});
		searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
			@Override
			public boolean onMenuItemActionExpand(final MenuItem item) {
				return true;
			}

			@Override
			public boolean onMenuItemActionCollapse(final MenuItem item) {
				Log.i("a", "Closed");
				Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
				if (fragment instanceof ExperienceSearchFragment) {
					getSupportFragmentManager().popBackStack();
				}
				return true;
			}
		});

		return true;
	}

	private void search(String query) {
		Analytics.logSearch(query);
		Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
		if (fragment instanceof ExperienceSearchFragment) {
			((ExperienceSearchFragment) fragment).search(query);
		} else {
			ExperienceSearchFragment experienceSearchFragment = new ExperienceSearchFragment();
			experienceSearchFragment.setArguments(new Bundle());
			experienceSearchFragment.getArguments().putString("query", query);
			navigate(experienceSearchFragment, true);
		}
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		if (requestCode == REQUEST_CODE_PICK_ACCOUNT) {
			if (resultCode == RESULT_OK) {
				if (data != null) {
					if (data.hasExtra(AccountManager.KEY_ACCOUNT_NAME)) {
						String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);

						if (accountName != null) {
							tryGoogleAccount(accountName, null);
						}
					}
				}
			}
		} else if (requestCode == REQUEST_CODE_SIGN_IN) {
			final GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
			if (result.getSignInAccount() != null) {
				tryGoogleAccount(result.getSignInAccount().getEmail(), result.getSignInAccount().getDisplayName());
			}
		}
	}

	@Override
	protected void onCreate(final Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		if (Features.show_welcome.isEnabled(this)) {