public class Settings extends AppCompatActivity {

	public final static int SANS_SERIF = 0;
	public final static int SERIF = 1;

	/**
	 * An enum for the old text size format, before it was customizable. Retained only for
	 * compatibility
	 */
	private enum TextSize{
		SMALL(14,"S"),
		MEDIUM(18,"M"),
		LARGE(22,"L"),
		XLARGE(32,"XL");

		private int size;
		private String key;

		TextSize(int fontSize, String key){
			size = fontSize;
			this.key = key;
		}

		public static int getSize(String key){
			for (TextSize t : values()) {
				if (t.key.equals(key)) {
					return t.size;
				}
			}
			return MEDIUM.size;
		}
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		Settings.setOrientationAndTheme(this);
		super.onCreate(savedInstanceState);
		assert getSupportActionBar() != null;
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);

		if (savedInstanceState == null) {
			FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
			ft.replace(android.R.id.content, new PrefsFragment()).commit();
		}
	}

	public static class PrefsFragment extends PreferenceFragment implements OnPreferenceChangeListener, OnPreferenceClickListener{

		@Override
		public void onCreate(Bundle paramBundle) {
			super.onCreate(paramBundle);
			addPreferencesFromResource(R.xml.preferences);

			// Check if the orientation setting changes in order to update the activity orientation
			final Preference orientationPref = findPreference(getString(R.string.pref_key_orientation));
			orientationPref.setOnPreferenceChangeListener(this);

			// Only display the save location setting if there is an external storage available
			final boolean isSdCardAvailable = FileHandler.isExternalStorageWritable(getActivity());
			final Preference installLocation = findPreference(getString(R.string.pref_key_loc));
			installLocation.setEnabled(isSdCardAvailable);
			installLocation.setOnPreferenceChangeListener(this);

			// Check if the theme is changed in order to recreate the activity
			final Preference themeChanged = findPreference(getString(R.string.pref_key_theme));
			themeChanged.setOnPreferenceChangeListener(this);

			// Check if the locale is changed in order to recreate the activity
			final Preference localeChanged = findPreference(getString(R.string.pref_key_locale));
			localeChanged.setOnPreferenceChangeListener(this);

			// Check if the backup button is clicked
			final Preference backup = findPreference(getString(R.string.pref_key_back_up));
			backup.setOnPreferenceClickListener(this);

			// Check if the restore button is clicked
			final Preference restore = findPreference(getString(R.string.pref_key_restore));
			restore.setEnabled(RestoreDialog.findBackUpFile(getActivity()) != null);
			restore.setOnPreferenceClickListener(this);

			// Check if the font button is clicked
			final Preference fontDialog = findPreference(getString(R.string.pref_key_text_size));
			fontDialog.setOnPreferenceClickListener(this);

			// Check if the crash reporting preference has changed
			final Preference crashReporting = findPreference(getString(R.string.pref_key_crash_reporting));
			crashReporting.setOnPreferenceChangeListener(this);
		}

		@Override
		public boolean onPreferenceChange(Preference preference, Object newValue) {
			switch (preference.getKey()) {
				case "Application Orientation":
					// If the requested orientation changes, update the orientation
					final String value = (String) newValue;
					switch (value){
						case "A":
							getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
							break;
						case "H":
							getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
							break;
						default:
							getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
							break;
					}
					return true;
				case "Save Location":
					// Warn the user that changing the location does not move old stories
					showMoveDialog();
					return true;
				case "Application Theme":
					// Saves the preference, then reopens the settings file if
					// the new value is different.
					final String currentValue = preference.getSharedPreferences()
							.getString(getString(R.string.pref_key_theme), "D");

					if (currentValue.equals(newValue)) {
						return false;
					} else {
						final Editor editor = preference.getEditor();
						editor.putString(preference.getKey(), (String) newValue);
						editor.commit();

						final Intent i = getActivity().getIntent();
						getActivity().finish();
						startActivity(i);
						return false;
					}
				case "Locale":
					// Save the preference. If the selected locale is different from the current
					// one, recreate the activity
					final String currentLocale = preference.getSharedPreferences()
							.getString(getString(R.string.pref_key_locale), "auto");

					if (currentLocale.equals(newValue)) {
						return false;
					} else {
						final Editor editor = preference.getEditor();
						editor.putString(preference.getKey(), (String) newValue);
						editor.commit();

						final Intent i = getActivity().getIntent();
						getActivity().finish();
						startActivity(i);
						return false;
					}
				case "Automatically Report Crashes":
					FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled((boolean) newValue);
					return true;
				default:
					return true;
			}
		}

		private void showMoveDialog() {
			AlertDialog.Builder dialog = new Builder(getActivity());
			dialog.setTitle(R.string.pref_loc_diag_title);
			dialog.setMessage(R.string.pref_loc_diag);
			dialog.setNeutralButton(android.R.string.ok, null);
			dialog.show();
		}

		@Override
		public boolean onPreferenceClick(Preference preference) {
			if (preference.getKey().equals(getString(R.string.pref_key_back_up))) {
				DialogFragment dialog = new BackUpDialog();
				dialog.show(getFragmentManager(), dialog.getClass().getName());
			}else if (preference.getKey().equals(getString(R.string.pref_key_restore))){
				DialogFragment dialog = new RestoreDialogConfirmation();
				dialog.show(getFragmentManager(), dialog.getClass().getName());
			}else if(preference.getKey().equals(getString(R.string.pref_key_text_size))){
				DialogFragment dialog = new FontDialog();
				dialog.show(getFragmentManager(), dialog.getClass().getName());
			}
			return false;
		}
	}


	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case android.R.id.home:
			onBackPressed();
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}

	/**
	 * Gets the font size that should be used to display the story text. The font size is returned
	 * as an integer. The font size should be interpreted as scale independent pixels, SP.
	 * @param context The current context
	 * @return The font size, as an integer
	 */
	public static int fontSize(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);

		int textSize;
		try {
			//Try to get the text size as an integer
			textSize = sharedPref.getInt(context.getString(R.string.pref_key_text_size), 14);
		} catch (ClassCastException e) {
			//Else, get the old format and convert it to the new one
			textSize = TextSize.getSize(sharedPref.getString(context.getString(R.string.pref_key_text_size), ""));
			Editor editor = sharedPref.edit();
			editor.putInt(context.getString(R.string.pref_key_text_size), textSize);
			editor.apply();
		}

		return textSize;
	}

	/**
	 * Checks if the stories should be updated incrementally. If enabled, only new chapters should
	 * be downloaded. If false, every old chapter should be redownloaded in order to account for
	 * possible changes.
	 * @param context The current context
	 * @return True if chapters should be updated incrementally, false otherwise
	 */
	public static boolean isIncrementalUpdatingEnabled(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
		return sharedPref.getBoolean(context.getString(R.string.pref_incremental_updating), true);
	}

	/**
	 * Checks if the volume keys should be used to scroll in stories. If true, the volume keys
	 * should be able to scroll a chapter.
	 * @param context The current context
	 * @return True if volume key scrolling is enabled
	 */
	public static boolean volumeButtonsScrollStory(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
		return sharedPref.getBoolean(context.getString(R.string.pref_key_volume_scroll), false);
	}

	/**
	 * Checks if the wake lock should be enabled. If true, the device should not dim the screen
	 * due to inactivity while displaying a story.
	 * @param context The current context
	 * @return True if the wake lock should be enabled
	 */
	public static boolean isWakeLockEnabled(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
		return sharedPref.getBoolean(context.getString(R.string.pref_key_wake_lock), true);
	}

	/**
	 * Checks if the crash reporting should be enabled. If true, the device should report crashes
	 * @param context The current context
	 * @return True if crashes should be reported
	 */
	public static boolean isCrashReportingEnabled(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
		return sharedPref.getBoolean(context.getString(R.string.pref_key_crash_reporting), false);
	}

	public static boolean shouldWriteToSD(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
		return sharedPref.getString(context.getString(R.string.pref_key_loc), "ext").equals("ext");
	}

	public static int getTypeFaceId(Context context){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
		return sharedPref.getInt(context.getString(R.string.pref_key_type_face), 0);
	}

	public static Typeface getTypeFace(Context context){
		switch (getTypeFaceId(context)) {
		case SANS_SERIF:
			return Typeface.SANS_SERIF;
		case SERIF:
			return Typeface.SERIF;
		default:
			return Typeface.DEFAULT;
		}
	}

	/**
	 * Sets the orientation of the activity based on the current user settings.
	 * @param activity The application whose settings should be changed
	 * @param sharedPref The application's default SharedPreferences
	 */
	private static void setOrientation(Activity activity, SharedPreferences sharedPref){
		String orientation = sharedPref.getString(activity.getString(R.string.pref_key_orientation), "A");
		switch (orientation){
			case "A":	// Automatic orientation
				activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
				break;
			case "H":	// Landscape orientation
				activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
				break;
			default:	// Vertical orientation
				activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
				break;
		}
	}

	/**
	 * Sets the application locale, overriding the default if required by the user
	 *
	 * @param context    The current context
	 * @param sharedPref The application's default SharedPreferences
	 */
	private static void setLocale(Context context, SharedPreferences sharedPref) {
		// Obtain the required locale
		final Locale locale;
		String languageCode = sharedPref.getString(context.getString(R.string.pref_key_locale),
												   context.getString(R.string.pref_locale_default));
		if (languageCode.equals(context.getString(R.string.pref_locale_default))) {
			locale = Locale.getDefault();
		} else {
			locale = new Locale(languageCode);
		}

		// Set the locale
		Resources res = context.getResources();
		DisplayMetrics metrics = res.getDisplayMetrics();
		Configuration configuration = res.getConfiguration();
		configuration.locale = locale;
		res.updateConfiguration(configuration, metrics);
	}

	/**
	 * Sets the orientation of the activity based on current settings
	 *
	 * @param activity The activity to set
	 */
	public static void setOrientationAndTheme(Activity activity) {
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(activity);
		setOrientation(activity, sharedPref);
		setLocale(activity, sharedPref);

		String theme = sharedPref.getString(activity.getString(R.string.pref_key_theme), "D");
		switch (theme) {
			case "DD":
				activity.setTheme(R.style.AppActionBar_Darker);
				break;
			case "D":
				activity.setTheme(R.style.AppActionBar);
				break;
			default:
				activity.setTheme(R.style.AppActionBarLight);
				break;
		}
	}

 	public static int getDialogTheme(Context context){
 		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
 		String theme = sharedPref.getString(context.getString(R.string.pref_key_theme), "D");
		if (theme.equals("DD") || theme.equals("D")){
			return R.style.DialogDark;
		}else{
			return R.style.DialogLight;
		}
 	}

 	/**
 	 * Sets the orientation of the activity based on current settings
	 * @param activity The activity to set
	 */
 	public static void setOrientationAndThemeNoActionBar(Activity activity){
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(activity);
		setOrientation(activity, sharedPref);
		setLocale(activity, sharedPref);

		String theme = sharedPref.getString(activity.getString(R.string.pref_key_theme), "D");
		switch (theme){
			case "DD":	//Materials Darker
				activity.setTheme(R.style.MaterialDarker);
				break;
			case "D":	//Materials Dark
				activity.setTheme(R.style.MaterialDark);
				break;
			default:	//Materials Light
				activity.setTheme(R.style.MaterialLight);
				break;
		}
	}
} public abstract class BaseFragment<T extends Parcelable> extends Fragment
		implements LoaderCallbacks<List<T>>, OnClickListener {

	/**
	 * Used to avoid a TransactionTooLargeException, the data is stored in a persistent fragment
	 */
	private DataFragment mDataFragment;

	/**
	 * The loader's saved instance state.
	 */
	protected Bundle mLoaderArgs;

	protected ListView mListView;
	private BaseAdapter mAdapter;

	private List<T> mList;

	// Views located in the footer
	private Button mAddPageButton;
	private View mErrorBar;
	private View mProgressBar;
	private TextView mRetryLabel;
	private FrameLayout mEmptyView;

	/**
	 * The most recently used {@link BaseLoader}
	 */
	protected BaseLoader<T> mLoader;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

		mList = new ArrayList<>();

		// Get the listView and the empty view
		View v = inflater.inflate(R.layout.activity_list_view, container, false);
		mListView = v.findViewById(android.R.id.list);
		mEmptyView = v.findViewById(R.id.empty);

		// Set the footer and its associated variables
		View footer = inflater.inflate(R.layout.footer_list, mListView, false);
		mProgressBar = footer.findViewById(R.id.progress_bar);
		mErrorBar = footer.findViewById(R.id.row_retry);
		mRetryLabel = footer.findViewById(R.id.label_retry);
		mAddPageButton = footer.findViewById(R.id.story_load_pages);
		mAddPageButton.setOnClickListener(this);
		final View retryButton = footer.findViewById(R.id.btn_retry);
		retryButton.setOnClickListener(this);

		mListView.addFooterView(footer, null, false);

		mAdapter = adapter(mList);
		mListView.setAdapter(mAdapter);

		return v;
	}

	@Override
	public void onActivityCreated(@Nullable Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);

		// SavedInstanceState throws an TransactionTooLargeException when saving data,
		// so we save the data in a persistent fragment instead.
		// Create the persistent DataFragment if it doesn't exist and recover the saved bundle.
		mDataFragment = (DataFragment) getParentFragmentManager().findFragmentByTag("DATA");
		if (mDataFragment == null){
			mDataFragment = new DataFragment();
			getParentFragmentManager().beginTransaction().add(mDataFragment,"DATA").commit();
		}
		mLoaderArgs = mDataFragment.getData();
	}

	@Override
	public void onLoaderReset(@NonNull Loader<List<T>> loader) {
		mList = null;
	}

	@Override
	public void onLoadFinished(@NonNull Loader<List<T>> loader, List<T> data) {
		mLoader = (BaseLoader<T>) loader;

		mList.clear();
		mList.addAll(data);
		mAdapter.notifyDataSetChanged();

		// Get the current state. Use LOADING if the loader is not
		// available yet.
		Result state = mLoader.getState();

		// Make all the views invisible, then make the views visible as required.
		mProgressBar.setVisibility(View.GONE);
		mErrorBar.setVisibility(View.GONE);
		mAddPageButton.setVisibility(View.GONE);
		mEmptyView.setVisibility(View.GONE);

		// Unlike the footer, the listView should be visible by default. It is only
		// removed in order to show the empty view.
		mListView.setVisibility(View.VISIBLE);

		switch (state) {
			case LOADING:
				// Only the progress bar should be visible
				mProgressBar.setVisibility(View.VISIBLE);
				break;
			case LOADING_HIDE_PROGRESS:
				// No views should be visible. To be used for short local loads only
				break;
			case ERROR_CONNECTION:
				// Display a connection error
				mErrorBar.setVisibility(View.VISIBLE);
				mRetryLabel.setText(R.string.error_connection);
				break;
			case ERROR_PARSE:
				// Display a parsing error
				mErrorBar.setVisibility(View.VISIBLE);
				mRetryLabel.setText(R.string.error_parsing_mini);
				break;
			case ERROR_CLOUDFLARE_CAPTCHA:
				// Launch a new fragment
				final Uri uri = mLoader.getUri(mLoader.getCurrentPage());
				final Bundle arguments = new Bundle();
				arguments.putParcelable(CloudflareFragment.EXTRA_URI, uri);

				final FragmentManager manager = getParentFragmentManager();
				manager.setFragmentResultListener("DATA_CLOUDFLARE",this,(requestKey, bundle) ->{
					mLoader.setHtmlFromWebView(bundle.getString("DATA"));
					mLoader.startLoading();
				});

				manager.beginTransaction()
						.add(CloudflareFragment.class, arguments, "DATA_CLOUDFLARE")
						.setReorderingAllowed(true)
						.commit();

				// Only the progress bar should be visible
				mProgressBar.setVisibility(View.VISIBLE);
				break;
			case SUCCESS:
				// If the loader has additional pages, display the button
				if (mLoader.hasNextPage()) {
					String text = getString(R.string.menu_story_page_button, mLoader.getCurrentPage() + 1,
											mLoader.getTotalPages());
					mAddPageButton.setVisibility(View.VISIBLE);
					mAddPageButton.setText(text);
				}

				// If an empty view has been set and the data set is empty, show the empty view.
				if (data.isEmpty() && mEmptyView.getChildCount() > 0){
					mListView.setVisibility(View.GONE);
					mEmptyView.setVisibility(View.VISIBLE);
				}
			default:
				break;
		}

	}

	@Override
	public void onSaveInstanceState(@NonNull Bundle outState) {
		super.onSaveInstanceState(outState);

		// Since the savedInstanceState throws an error with large bundles, the state is saved
		// in a persistent fragment instead.
		final Bundle savedState = new Bundle();
		mLoader.saveInstanceState(savedState);
		mDataFragment.saveData(savedState);
	}

	protected abstract BaseAdapter adapter(List<T> dataSet);

	/**
	 * Sets the subTitle of the action bar.
	 * @param title The subTitle
	 */
	protected final void setSubTitle(@StringRes int title) {
		final ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
		if (actionBar != null) {
			actionBar.setSubtitle(title);
		} else {
			Log.e(this.getClass().getName(), "The subtitle cannot be changed because getSupportActionBar is null");
		}
	}

	/**
	 * Sets the subTitle of the action bar.
	 * @param title The subTitle
	 */
	protected final void setSubTitle(String title) {
		final ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
		if (actionBar != null) {
			actionBar.setSubtitle(title);
		} else {
			Log.e(this.getClass().getName(), "The subtitle cannot be changed because getSupportActionBar is null");
		}
	}

	protected final void setTitle(@StringRes int title) {
		requireActivity().setTitle(title);
	}

	@Override
	public void onClick(View v) {
		if (mLoader == null) return;
		if (v.getId() == R.id.btn_retry){
			mLoader.setHtmlFromWebView(null);
			mLoader.startLoading();
		} else if (v.getId() == R.id.story_load_pages){
			mLoader.loadNextPage();
		}
	}

	protected T getItem(int position) {
		return mList.get(position);
	}

	/**
	 * Gets a loader
	 * @author Michael Chen
	 *
	 * @param <T> The type of data handled by the loader.
	 */
	protected interface LoaderAdapter<T extends Parcelable> {
		BaseLoader<T> getNewLoader(Bundle args);
	}

	/**
	 * Sets a view that will be displayed in the center of the screen when the loader succeeds but
	 * returns an empty data set.
	 *
	 * @param emptyView The view that should be displayed upon an empty data set.
	 */
	public final void setEmptyView(View emptyView){
		mEmptyView.removeAllViews();
		mEmptyView.addView(emptyView);
	}
} protected final static class ArchiveOfOurOwnBrowseLoader extends BaseLoader<BrowseMenuItem> {

	public ArchiveOfOurOwnBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0; // There are no additional pages
	}

	@Override
	protected Uri getUri(int currentPage) {
		return Sites.ARCHIVE_OF_OUR_OWN.BASE_URI;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		Elements fandoms = document.select("div.browse.module a");

		if (fandoms.isEmpty()) { return false; }

		for (Element element : fandoms) {
			final Uri url = Uri.parse(element.attr("abs:href"));
			final String title = element.ownText();

			// Skip the "All Fandoms" category
			if (url.equals(Uri.parse("http://archiveofourown.org/media"))) {
				continue;
			}

			list.add(new BrowseMenuItem(title, url));
		}
		return true;
	}

} protected final static class FanFictionCommunityBrowseLoader extends BaseLoader<BrowseMenuItem> {
	private static final String[] FANFIC_URLS = { "general/0", "anime", "book", "cartoon", "comic", "game", "misc",
			"movie", "play", "tv" };
	private final String[] categories;

	public FanFictionCommunityBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
		disableProgressBar();
		categories = context.getResources().getStringArray(R.array.browse_menu_ff_community);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0; // There are no additional pages
	}

	@Override
	protected Uri getUri(int currentPage) {
		return null;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		for (int i = 0; i < categories.length; i++) {
			Uri.Builder builder = Sites.FANFICTION.BASE_URI.buildUpon();
			builder.appendPath("communities");
			builder.appendEncodedPath(FANFIC_URLS[i]);
			builder.appendPath("");
			list.add(new BrowseMenuItem(categories[i], builder.build()));
		}
		return true;
	}
} protected final static class FanFictionCrossOverBrowseLoader extends BaseLoader<BrowseMenuItem> {
	private static final String[] FANFIC_URLS = { "anime", "book", "cartoon", "comic", "game", "misc", "movie",
			"play", "tv" };
	private final String[] categories;

	public FanFictionCrossOverBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
		disableProgressBar();
		categories = context.getResources().getStringArray(R.array.browse_menu_ff);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0; // There are no additional pages
	}

	@Override
	protected Uri getUri(int currentPage) {
		return null;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		for (int i = 0; i < categories.length; i++) {
			Uri.Builder builder = Sites.FANFICTION.BASE_URI.buildUpon();
			builder.appendPath("crossovers");
			builder.appendPath(FANFIC_URLS[i]);
			builder.appendPath("");
			list.add(new BrowseMenuItem(categories[i], builder.build()));
		}
		return true;
	}

} protected final static class FanFictionRegularBrowseLoader extends BaseLoader<BrowseMenuItem> {
	private static final String[] FANFIC_URLS = { "anime", "book", "cartoon", "comic", "game", "misc", "movie",
			"play", "tv" };
	private final String[] categories;

	public FanFictionRegularBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
		disableProgressBar();
		categories = context.getResources().getStringArray(R.array.browse_menu_ff);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0; // There are no additional pages
	}

	@Override
	protected Uri getUri(int currentPage) {
		return null;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		for (int i = 0; i < categories.length; i++) {
			Uri.Builder builder = Sites.FANFICTION.BASE_URI.buildUpon();
			builder.appendPath(FANFIC_URLS[i]);
			builder.appendPath("");
			list.add(new BrowseMenuItem(categories[i], builder.build()));
		}
		return true;
	}
} protected final static class FictionPressCommunityBrowseLoader extends BaseLoader<BrowseMenuItem> {
	private static final String[] FANFIC_URLS = { "general/0", "fiction", "poetry" };
	private final String[] categories;

	public FictionPressCommunityBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
		disableProgressBar();
		categories = context.getResources().getStringArray(R.array.browse_menu_fp_community);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0;
	}

	@Override
	protected Uri getUri(int currentPage) {
		return null;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		for (int i = 0; i < categories.length; i++) {
			Uri.Builder builder = Sites.FICTIONPRESS.BASE_URI.buildUpon();
			builder.appendPath("communities");
			builder.appendPath(FANFIC_URLS[i]);
			builder.appendPath("");
			list.add(new BrowseMenuItem(categories[i], builder.build()));
		}
		return true;
	}

} protected final static class FictionPressPoetryPressBrowseLoader extends BaseLoader<BrowseMenuItem> {

	public FictionPressPoetryPressBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0; // There are no additional pages
	}

	@Override
	protected Uri getUri(int currentPage) {
		return Sites.FICTIONPRESS.BASE_URI;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		Elements fiction = document.select("#gui_table2i a");

		if (fiction.isEmpty()) return false;

		for (Element element : fiction) {
			final Uri url = Uri.parse(element.attr("abs:href"));
			final String title = element.ownText();
			list.add(new BrowseMenuItem(title, url));
		}

		return true;
	}

} protected final static class FictionpressFictionPressBrowseLoader extends BaseLoader<BrowseMenuItem> {

	public FictionpressFictionPressBrowseLoader(Context context, Bundle savedInstanceState) {
		super(context, savedInstanceState);
	}

	@Override
	protected int getTotalPages(Document document) {
		return 0; // There are no additional pages
	}

	@Override
	protected Uri getUri(int currentPage) {
		return Sites.FICTIONPRESS.BASE_URI;
	}

	@Override
	protected boolean load(Document document, List<BrowseMenuItem> list) {
		Elements fiction = document.select("#gui_table1i a");

		if (fiction.isEmpty()) { return false; }

		for (Element element : fiction) {
			final Uri url = Uri.parse(element.attr("abs:href"));
			final String title = element.ownText();
			list.add(new BrowseMenuItem(title, url));
		}

		return true;
	}

} public class CategoryMenuActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		Settings.setOrientationAndThemeNoActionBar(this);
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_frame_layout);

		Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
		setSupportActionBar(toolbar);
		Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);

		if (savedInstanceState == null) {
			FragmentTransaction fr = getSupportFragmentManager().beginTransaction();
			fr.replace(R.id.content_frame, new CategoryMenuFragment());
			fr.commit();
		}
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if (item.getItemId() == android.R.id.home){
			onBackPressed();
			return true;
		} else{
			return super.onOptionsItemSelected(item);
		}
	}

	public final static class CategoryMenuFragment extends BaseFragment<CategoryMenuItem> {

		private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

		private static final String STATE_SORT = "STATE_SORT";
		private static final int FANFICTION_REGULAR = 0;
		private static final int FANFICTION_CROSSOVER = 1;
		private static final int FANFICTION_SUB_CATEGORY = 2;
		private static final int FANFICTION_COMMUNITY = 3;
		private static final int ARCHIVE_OF_OUR_OWN_NORMAL = 4;
		private static final int FICTIONPRESS_COMMUNITY = 5;

		static {
			// Archive Of Our Own
			URI_MATCHER.addURI(Sites.ARCHIVE_OF_OUR_OWN.AUTHORITY, "media/*/fandoms", ARCHIVE_OF_OUR_OWN_NORMAL);

			// FanFiction Mobile Site
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "crossovers/*/", FANFICTION_CROSSOVER);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "crossovers/*/#/", FANFICTION_SUB_CATEGORY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "communities/*/", FANFICTION_COMMUNITY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "*/", FANFICTION_REGULAR);
			// FanFiction Desktop Site
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "crossovers/*/", FANFICTION_CROSSOVER);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "crossovers/*/#/", FANFICTION_SUB_CATEGORY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "communities/*/", FANFICTION_COMMUNITY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "*/", FANFICTION_REGULAR);

			// FictionPress Mobile Site
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "communities/*/", FICTIONPRESS_COMMUNITY);
			// FictionPress Desktop Site
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "communities/*/", FICTIONPRESS_COMMUNITY);
		}

		private LoaderAdapter<CategoryMenuItem> mLoaderAdapter;
		private CategoryMenuAdapter mAdapter;
		private boolean mSortOrder;

		@Override
		public void onActivityCreated(Bundle savedInstanceState) {
			super.onActivityCreated(savedInstanceState);
			setHasOptionsMenu(true);

			final Uri uri = requireActivity().getIntent().getData();
			int site = URI_MATCHER.match(uri);

			switch (site) {
			case ARCHIVE_OF_OUR_OWN_NORMAL:
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					final Intent i = new Intent(getActivity(), StoryMenuActivity.class);
					i.setData(getItem(position).mUri);
					startActivity(i);
				});
				mLoaderAdapter = args -> new ArchiveOfOurOwnCategoryLoader(getActivity(), args, uri);
				setTitle(R.string.menu_navigation_title_regular);
				String subTitle = WordUtils.capitalize(uri.getPathSegments().get(1), ' ', '.', '\'');
				setSubTitle(subTitle.replace("*a*", "&"));
				break;
			case FANFICTION_REGULAR:
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					final Intent i = new Intent(getActivity(), StoryMenuActivity.class);
					i.setData(getItem(position).mUri);
					startActivity(i);
				});
				mLoaderAdapter = args -> new FanFictionRegularCategoryLoader(getActivity(), args, uri);
				setTitle(R.string.menu_navigation_title_regular);
				setSubTitle(WordUtils.capitalize(uri.getPathSegments().get(0), ' ', '.', '\''));
				break;
			case FANFICTION_CROSSOVER:
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					final Intent i = new Intent(getActivity(), CategoryMenuActivity.class);
					i.setData(getItem(position).mUri);
					startActivity(i);
				});
				mLoaderAdapter = args -> new FanFictionRegularCategoryLoader(getActivity(), args, uri);
				setTitle(R.string.menu_navigation_title_crossover);
				setSubTitle(WordUtils.capitalize(uri.getPathSegments().get(1), ' ', '.', '\''));
				break;
			case FANFICTION_SUB_CATEGORY:
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					final Intent i = new Intent(getActivity(), StoryMenuActivity.class);
					i.setData(getItem(position).mUri);
					startActivity(i);
				});
				mLoaderAdapter = args -> new FanFictionSubCategoryLoader(getActivity(), args, uri);
				setTitle(R.string.menu_navigation_title_crossover);
				setSubTitle(WordUtils.capitalize(uri.getPathSegments().get(1), ' ', '.', '\''));
				break;
			case FANFICTION_COMMUNITY:
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					final Intent i = new Intent(getActivity(), CommunityMenuActivity.class);
					i.setData(getItem(position).mUri);
					startActivity(i);
				});
				mLoaderAdapter = args -> new FanFictionCommunityCategoryLoader(getActivity(), args, uri);
				setTitle(R.string.menu_navigation_title_community);
				setSubTitle(WordUtils.capitalize(uri.getPathSegments().get(1), ' ', '.', '\''));
				break;
			case FICTIONPRESS_COMMUNITY:
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					final Intent i = new Intent(getActivity(), CommunityMenuActivity.class);
					i.setData(getItem(position).mUri);
					startActivity(i);
				});
				mLoaderAdapter = args -> new FictionPressCommunityCategoryLoader(getActivity(), args, uri);
				setTitle(R.string.menu_navigation_title_community);
				setSubTitle(WordUtils.capitalize(uri.getPathSegments().get(1), ' ', '.', '\''));
				break;
			default:
				throw new IllegalArgumentException("The uri " + uri + " is invalid.");
			}

			if (savedInstanceState != null) {
				mSortOrder = savedInstanceState.getBoolean(STATE_SORT);
				mListView.setFastScrollEnabled(mSortOrder == CategoryMenuComparator.SORT_ALPHABETICAL);
			} else {
				mSortOrder = CategoryMenuComparator.SORT_VIEWS;
			}

			LoaderManager.getInstance(this).initLoader(0, mLoaderArgs, this);
		}

		@Override
		public void onSaveInstanceState(@NonNull Bundle outState) {
			super.onSaveInstanceState(outState);
			outState.putBoolean(STATE_SORT, mSortOrder);
		}

		@Override
		public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
			// Inflate the menu
			inflater.inflate(R.menu.navigation_menu, menu);

			MenuItem filter = menu.findItem(R.id.filter);
			if (mLoader != null && mLoader instanceof Filterable) {
				filter.setVisible(true);

				// Set alpha, since the drawable's alpha may have been changed
				// by other activities
				filter.getIcon().setAlpha(255);
			} else {
				filter.setVisible(false);
			}
		}

		@Override
		public void onLoadFinished(@NonNull Loader<List<CategoryMenuItem>> loader, List<CategoryMenuItem> data) {
			super.onLoadFinished(loader, data);
			mAdapter.sort(mSortOrder);
			requireActivity().invalidateOptionsMenu();
		}

		@Override
		public void onResume() {
			super.onResume();
			requireActivity().invalidateOptionsMenu();
		}

		@Override
		public boolean onOptionsItemSelected(MenuItem item) {
			if (item.getItemId() == R.id.navigation_library_sort_by_name){
				if (mSortOrder != CategoryMenuComparator.SORT_ALPHABETICAL) {
					mListView.setFastScrollEnabled(true);
					mSortOrder = CategoryMenuComparator.SORT_ALPHABETICAL;
					mAdapter.sort(mSortOrder);
				}
				return true;
			} else if(item.getItemId() == R.id.navigation_library_sort_by_size){
				if (mSortOrder != CategoryMenuComparator.SORT_VIEWS) {
					mListView.setFastScrollEnabled(false);
					mSortOrder = CategoryMenuComparator.SORT_VIEWS;
					mAdapter.sort(mSortOrder);
				}
				return true;
			} else if (item.getItemId() == R.id.filter){
				displayFilterDialog();
				return true;
			} else{
				return super.onOptionsItemSelected(item);
			}
		}

		/**
		 * Displays the dialog that handles filtering
		 */
		private void displayFilterDialog() {
			AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

			final Filterable loader = (Filterable) mLoader;

			String[] filterList = loader.getFilterEntries();

			builder.setItems(filterList, (dialog, position) -> {
				loader.onFilterSelected(position);
				mSortOrder = CategoryMenuComparator.SORT_ALPHABETICAL;
			});
			builder.show();
		}

		@NonNull
		@Override
		public Loader<List<CategoryMenuItem>> onCreateLoader(int id, Bundle args) {
			return mLoaderAdapter.getNewLoader(args);
		}

		@Override
		protected BaseAdapter adapter(List<CategoryMenuItem> dataSet) {
			return mAdapter = new CategoryMenuAdapter(getActivity(), dataSet);
		}

		protected interface Filterable {

			String[] getFilterEntries();

			void onFilterSelected(int position);

		}

	}
} public class CommunityMenuActivity extends AppCompatActivity {

	public static final class CommunityMenuFragment extends BaseFragment<CommunityMenuItem>{
		
		/**
		 * An interface that represents sortable members.
		 * @author Michael Chen
		 *
		 */
		interface Sortable{
			void sort(SortBy sortKey);
		}
		
		private static final int ARCHIVE_OF_OUR_OWN_COLLECTION = 2;
		private static final int FANFICTION_COMMUNITY = 0;
		private static final int FICTIONPRESS_COMMUNITY = 1;
		
		private static final UriMatcher URI_MATCHER;

		static{
			URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
			
			// FanFiction Communities
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "communities/general/", FANFICTION_COMMUNITY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "communities/*/*/", FANFICTION_COMMUNITY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "communities/general/", FANFICTION_COMMUNITY);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "communities/*/*/", FANFICTION_COMMUNITY);
			
			// FictionPress Communities
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "communities/general/", FICTIONPRESS_COMMUNITY);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "communities/*/*/", FICTIONPRESS_COMMUNITY);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "communities/general/", FICTIONPRESS_COMMUNITY);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "communities/*/*/", FICTIONPRESS_COMMUNITY);
			
			// Archive of Our Own Collections
			URI_MATCHER.addURI(Sites.ARCHIVE_OF_OUR_OWN.AUTHORITY, "collections", ARCHIVE_OF_OUR_OWN_COLLECTION);
		}
		
		private LoaderAdapter<CommunityMenuItem> mLoaderAdapter;

		@Override
		protected BaseAdapter adapter(List<CommunityMenuItem> dataSet) {
			return new CommunityAdapter(getActivity(), dataSet);
		}

		@Override
		public void onActivityCreated(Bundle savedInstanceState) {
			super.onActivityCreated(savedInstanceState);
			setHasOptionsMenu(true);

			// Match the link, without any filters
			final Uri uri = removeFilters(getActivity().getIntent().getData());
			final int site = URI_MATCHER.match(uri);

			switch (site) {
			case FANFICTION_COMMUNITY:
				mLoaderAdapter = args -> new FanFictionCommunityLoader(getActivity(), args, uri);
				setTitle(R.string.menu_button_communities);
				setSubTitle(WordUtils.capitalize(uri.getLastPathSegment()));
				break;
			case FICTIONPRESS_COMMUNITY:
				mLoaderAdapter = args -> new FanFictionCommunityLoader(getActivity(), args, uri);
				setTitle(R.string.menu_button_communities);
				setSubTitle(WordUtils.capitalize(uri.getLastPathSegment()));
				break;
			case ARCHIVE_OF_OUR_OWN_COLLECTION:

				break;
			default:
				throw new IllegalStateException("CommunityMenuActivity: Invalid Uri" + uri);
			}
			
			mListView.setOnItemClickListener((parent, view, position, id) -> {
				final Intent i = new Intent(getActivity(), StoryMenuActivity.class);
				i.setData(getItem(position).uri);
				startActivity(i);
			});

			getLoaderManager().initLoader(0, mLoaderArgs, this);
		}

		@Override
		public Loader<List<CommunityMenuItem>> onCreateLoader(int id, Bundle args) {
			return mLoaderAdapter.getNewLoader(args);
		}
		
		@Override
		public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
			if (mLoader instanceof Sortable) {
				inflater.inflate(R.menu.community_menu, menu);				
			}
	    }

		@Override
		public boolean onOptionsItemSelected(MenuItem item){
			if (mLoader instanceof Sortable) {
				// Item can be sorted, pass the request to the loader				
				switch (item.getItemId()) {
				case R.id.community_sort_random:
					((Sortable) mLoader).sort(SortBy.RANDOM);
					return true;
				case R.id.community_sort_staff:
					((Sortable) mLoader).sort(SortBy.STAFF);
					return true;
				case R.id.community_sort_stories:
					((Sortable) mLoader).sort(SortBy.STORIES);
					return true;
				case R.id.community_sort_follows:
					((Sortable) mLoader).sort(SortBy.FOLLOWS);
					return true;
				case R.id.community_sort_date:
					((Sortable) mLoader).sort(SortBy.CREATE_DATE);
					return true;
				}
			}
			return false;
		}
	
		/**
		 * Removes any path elements that begin with a digit
		 * 
		 * @param uri
		 *            The {@link Uri} that should be formatted
		 * @return The formatted Uri.
		 */
		private Uri removeFilters(Uri uri) {
			// Remove any prior filters by only copying the segment of the path
			// that does not start with a number.
			Builder builder = uri.buildUpon();
			builder.path(null);
			List<String> pathSegments = uri.getPathSegments();
			for (String pathSegment : pathSegments) {
				if (!Character.isDigit(pathSegment.charAt(0))) {
					builder.appendPath(pathSegment);
				} else {
					break;
				}
			}
			builder.appendPath("");
			return builder.build();
		}
	}
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		Settings.setOrientationAndThemeNoActionBar(this);
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_frame_layout);

		Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
		setSupportActionBar(toolbar);
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);

		if (savedInstanceState == null) {
			FragmentTransaction fr = getSupportFragmentManager().beginTransaction();
			fr.replace(R.id.content_frame, new CommunityMenuFragment());
			fr.commit();
		}
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case android.R.id.home:
			onBackPressed();
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}
	
} public class StoryMenuActivity extends AppCompatActivity implements FilterListener {
	
	private StoryMenuFragment mFragment; 

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		Settings.setOrientationAndThemeNoActionBar(this);
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_frame_layout);

		Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
		setSupportActionBar(toolbar);
		Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);

		final String tag = StoryMenuFragment.class.getName();
		if (savedInstanceState == null) {
			mFragment = new StoryMenuFragment();
			FragmentTransaction fr = getSupportFragmentManager().beginTransaction();
			fr.replace(R.id.content_frame, mFragment, tag);
			fr.commit();
		} else {
			mFragment = (StoryMenuFragment) getSupportFragmentManager().findFragmentByTag(tag);
			if (mFragment == null) {
				throw new RuntimeException("StoryMenuActivity - onCreate: mFragment == null");
			}
		}
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if (item.getItemId() == android.R.id.home){
			onBackPressed();
			return true;
		}{
			return super.onOptionsItemSelected(item);
		}
	}

	@Override
	public void onFilter(int[] selected) {
		mFragment.onFilter(selected);
	}
	
	public static final class StoryMenuFragment extends BaseFragment<Story>  implements FilterListener {
		// ArchiveOfOurOwn
		private static final int URI_AO3_NORMAL_MENU = 7;
		private static final int URI_AO3_COLLECTION_MENU = 8;
		// FanFiction
		private static final int URI_FF_NORMAL_MENU = 0;
		private static final int URI_FF_CROSSOVER_MENU = 1;
		private static final int URI_FF_JUST_IN_MENU = 2;
		private static final int URI_FF_COMMUNITY_MENU = 3;
		// FictionPress
		private static final int URI_FP_NORMAL_MENU = 4;
		private static final int URI_FP_JUST_IN_MENU = 5;
		private static final int URI_FP_COMMUNITY_MENU = 6;

		private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

		static {
			// Initializes the UriMatcher.

			// Archive Of Our Own Sites
			URI_MATCHER.addURI(Sites.ARCHIVE_OF_OUR_OWN.AUTHORITY, "tags/*/works", URI_AO3_NORMAL_MENU);
			URI_MATCHER.addURI(Sites.ARCHIVE_OF_OUR_OWN.AUTHORITY, "collections/*/works", URI_AO3_COLLECTION_MENU);

			// FanFiction Mobile Sites
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "j/", URI_FF_JUST_IN_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "community/*/#/", URI_FF_COMMUNITY_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "community/*/#/#/#/#/#/#/#/#/", URI_FF_COMMUNITY_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "*/#/#/", URI_FF_CROSSOVER_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "*/#/", URI_FF_NORMAL_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "*/*/", URI_FF_NORMAL_MENU);
			// FanFiction Desktop Sites
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "j/", URI_FF_JUST_IN_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "community/*/#/", URI_FF_COMMUNITY_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "community/*/#/#/#/#/#/#/#/#/", URI_FF_COMMUNITY_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "*/#/#/", URI_FF_CROSSOVER_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "*/#/", URI_FF_NORMAL_MENU);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "*/*/", URI_FF_NORMAL_MENU);

			// FictionPress Mobile Sites
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "j/", URI_FP_JUST_IN_MENU);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "community/*/#/", URI_FP_COMMUNITY_MENU);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "*/*/", URI_FP_NORMAL_MENU);
			// FictionPress Desktop Sites
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "j/", URI_FP_JUST_IN_MENU);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "community/*/#/", URI_FP_COMMUNITY_MENU);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "*/*/", URI_FP_NORMAL_MENU);
		}

		private LoaderAdapter<Story> mLoaderAdapter;

		@Override
		public void onActivityCreated(Bundle savedInstanceState) {
			super.onActivityCreated(savedInstanceState);

			setHasOptionsMenu(true);

			final Uri uri = requireActivity().getIntent().getData();
			String subTitle;

			switch (URI_MATCHER.match(uri)) {
			case URI_AO3_NORMAL_MENU:
				setTitle(R.string.menu_navigation_title_regular);
				subTitle = uri.getPathSegments().get(1);
				mLoaderAdapter = args -> new AO3RegularStoryLoader(getActivity(), args, uri);
				break;
			case URI_AO3_COLLECTION_MENU:
				setTitle(R.string.menu_navigation_title_community);
				subTitle = uri.getPathSegments().get(1);
				break;
			case URI_FF_NORMAL_MENU:
				setTitle(R.string.menu_navigation_title_regular);
				subTitle = uri.getLastPathSegment();
				mLoaderAdapter = args -> new FFRegularStoryLoader(getActivity(), args, uri);
				mListView.setOnItemClickListener((parent, view, position, id) -> StoryDisplayActivity.openStory(getActivity(), id, Site.FANFICTION, true));
				break;
			case URI_FF_CROSSOVER_MENU:
				setTitle(R.string.menu_navigation_title_crossover);
				subTitle = uri.getPathSegments().get(0);
				mLoaderAdapter = args -> new FFRegularStoryLoader(getActivity(), args, uri);
				mListView.setOnItemClickListener((parent, view, position, id) -> StoryDisplayActivity.openStory(getActivity(), id, Site.FANFICTION, true));
				break;
			case URI_FF_JUST_IN_MENU:
				setTitle(R.string.menu_story_title_just_in);
				subTitle = "";
				mLoaderAdapter = args -> new FFJustInStoryLoader(getActivity(), args, uri);
				mListView.setOnItemClickListener((parent, view, position, id) -> StoryDisplayActivity.openStory(getActivity(), id, Site.FANFICTION, true));
				break;
			case URI_FF_COMMUNITY_MENU:
				setTitle(R.string.menu_navigation_title_community);
				subTitle = uri.getPathSegments().get(1).replace('-', ' ');
				mLoaderAdapter = args -> new FFCommunityStoryLoader(getActivity(), args, uri);
				mListView.setOnItemClickListener((parent, view, position, id) -> StoryDisplayActivity.openStory(getActivity(), id, Site.FANFICTION, true));
				break;
			case URI_FP_NORMAL_MENU:
				setTitle(R.string.menu_navigation_title_regular);
				subTitle = uri.getLastPathSegment();
				mLoaderAdapter = args -> new FPRegularStoryLoader(getActivity(), args, uri);
				break;
			case URI_FP_JUST_IN_MENU:
				setTitle(R.string.menu_story_title_just_in);
				subTitle = "";
				break;
			case URI_FP_COMMUNITY_MENU:
				setTitle(R.string.menu_navigation_title_community);
				subTitle = uri.getLastPathSegment();
				break;
			default:
				throw new IllegalArgumentException("The uri " + uri + " is invalid.");
			}

			mListView.setOnItemLongClickListener((parent, view, position, id) -> {
				DetailDialog.show(getActivity(), (Story) parent.getItemAtPosition(position));
				return true;
			});
			setSubTitle(WordUtils.capitalize(subTitle));
			LoaderManager.getInstance(this).initLoader(0, mLoaderArgs, this);
		}

		@Override
		public void onResume() {
			super.onResume();
			requireActivity().invalidateOptionsMenu();
		}

		@Override
		public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
			inflater.inflate(R.menu.story_menu, menu);
		}

		@Override
		public void onPrepareOptionsMenu(Menu menu) {
			MenuItem filter = menu.findItem(R.id.filter);
			if (mLoader instanceof Filterable) {
				// If the menu can be filtered, enable the icon if ready
				filter.setVisible(true);
				if (((Filterable) mLoader).isFilterAvailable()) {
					filter.setEnabled(true);
					filter.getIcon().setAlpha(255);
				} else {
					filter.setEnabled(false);
					filter.getIcon().setAlpha(64);
				}
			} else {
				// If the menu cannot be filtered, hide the icon.
				filter.setVisible(false);
			}
		}

		@Override
		public boolean onOptionsItemSelected(MenuItem item) {
			if (item.getItemId() == R.id.filter){
				Filterable filterable = (Filterable) mLoader;
				filterable.onFilterClick(getActivity());
				return true;
			} else{
				return super.onOptionsItemSelected(item);
			}
		}

		@NonNull
		@Override
		public Loader<List<Story>> onCreateLoader(int id, Bundle args) {
			return mLoaderAdapter.getNewLoader(args);
		}

		@Override
		public void onLoadFinished(@NonNull Loader<List<Story>> loader, List<Story> data) {
			super.onLoadFinished(loader, data);
			requireActivity().invalidateOptionsMenu();
		}

		@Override
		protected BaseAdapter adapter(List<Story> dataSet) {
			return new StoryMenuAdapter(getActivity(), dataSet);
		}

		@Override
		public void onFilter(int[] selected) {
			Filterable filterable = (Filterable) mLoader;
			filterable.filter(selected);
		}		
	}
} public enum Sites {
	// Note to self: do not rename enum names. They are used as keys elsewhere
	ARCHIVE_OF_OUR_OWN("http", "archiveofourown.org", R.string.site_archive),
	FANFICTION("https", "m.fanfiction.net", "www.fanfiction.net", R.string.site_fanfiction), 
	FICTIONPRESS("https", "m.fictionpress.com","www.fictionpress.com", R.string.site_fictionpress);

	/**
	 * The base uri for the mobile version of the web site
	 */
	public final Uri BASE_URI;
	/**
	 * The base {@link Uri} for the desktop version of the web site
	 */
	public final Uri DESKTOP_URI;
	/**
	 * The authority for the mobile version of the web site
	 */
	public final String AUTHORITY;
	/**
	 * The authority for the desktop version of the web site
	 */
	public final String AUTHORITY_DESKTOP;
	/**
	 * The site's user friendly name
	 */
	public final int TITLE;

	Sites(@NonNull final String scheme, @NonNull final String authority, @StringRes final int name) {
		this(scheme, authority, null, name);
	}

	/**
	 * Creates a new site instance
	 * 
	 * @param scheme
	 *            The site's scheme
	 * @param authority
	 *            The mobile site authority
	 * @param desktopAuthority
	 *            The desktop site authority
	 * @param name
	 *            The site's user friendly name
	 */
	Sites(@NonNull final String scheme, @NonNull final String authority,
		  @Nullable final String desktopAuthority, @StringRes final int name) {

		// Generates the base Uri
		Uri.Builder builder = new Uri.Builder();
		builder.scheme(scheme);
		builder.authority(authority);
		BASE_URI = builder.build();

		// Sets the authority
		AUTHORITY = authority;

		if (desktopAuthority == null) {
			DESKTOP_URI = null;
			AUTHORITY_DESKTOP = null;
		} else {
			builder = new Uri.Builder();
			builder.scheme(scheme);
			builder.authority(desktopAuthority);
			DESKTOP_URI = builder.build();
			AUTHORITY_DESKTOP = desktopAuthority;
		}

		// Fetches the id's for each web site's name
		TITLE = name;
	}
}
import android.content.Intent;
import android.content.UriMatcher;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.FragmentTransaction;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.core.view.MenuItemCompat;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.BaseAdapter;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ToggleButton;
import com.spicymango.fanfictionreader.R;
import com.spicymango.fanfictionreader.Settings;
import com.spicymango.fanfictionreader.menu.BaseFragment;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.ArchiveOfOurOwnBrowseLoader;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.FanFictionCommunityBrowseLoader;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.FanFictionCrossOverBrowseLoader;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.FanFictionRegularBrowseLoader;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.FictionPressCommunityBrowseLoader;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.FictionPressPoetryPressBrowseLoader;
import com.spicymango.fanfictionreader.menu.browsemenu.BrowseMenuLoaders.FictionpressFictionPressBrowseLoader;
import com.spicymango.fanfictionreader.menu.categorymenu.CategoryMenuActivity;
import com.spicymango.fanfictionreader.menu.communitymenu.CommunityMenuActivity;
import com.spicymango.fanfictionreader.menu.storymenu.StoryMenuActivity;
import com.spicymango.fanfictionreader.util.Sites;
import java.util.List;
import java.util.Objects;
package com.spicymango.fanfictionreader.menu.browsemenu;





public class BrowseMenuActivity extends AppCompatActivity {

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if (item.getItemId() == android.R.id.home){
			onBackPressed();
			return true;
		} else{
			return super.onOptionsItemSelected(item);
		}
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		Settings.setOrientationAndThemeNoActionBar(this);
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_frame_layout);

		Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
		setSupportActionBar(toolbar);
		Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);

		if (savedInstanceState == null) {
			FragmentTransaction fr = getSupportFragmentManager().beginTransaction();
			fr.replace(R.id.content_frame, new BrowseMenuFragment());
			fr.commit();
		}
	}

	public final static class BrowseMenuFragment extends BaseFragment<BrowseMenuItem>
			implements OnCheckedChangeListener {

		private final static UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

		private final static int SITE_ARCHIVE_OF_OUR_OWN = 0;
		private final static int SITE_FANFICTION = 1;
		private final static int SITE_FANFICTION_COMMUNITY = 2;
		private final static int SITE_FICTIONPRESS = 3;
		private final static int SITE_FICTIONPRESS_COMMUNITY = 4;

		private final static String STATE_TOGGLE_BUTTON = "STATE_TOGGLE";

		static {
			//Archive of Our Own
			URI_MATCHER.addURI(Sites.ARCHIVE_OF_OUR_OWN.AUTHORITY, null, SITE_ARCHIVE_OF_OUR_OWN);
			
			//FanFiction Mobile
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, null, SITE_FANFICTION);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY, "communities/", SITE_FANFICTION_COMMUNITY);
			//FanFiction Desktop
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, null, SITE_FANFICTION);
			URI_MATCHER.addURI(Sites.FANFICTION.AUTHORITY_DESKTOP, "communities/", SITE_FANFICTION_COMMUNITY);
			
			//FictionPress Mobile
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, null, SITE_FICTIONPRESS);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY, "communities/", SITE_FICTIONPRESS_COMMUNITY);
			//FictionPress Desktop
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, null, SITE_FICTIONPRESS);
			URI_MATCHER.addURI(Sites.FICTIONPRESS.AUTHORITY_DESKTOP, "communities/", SITE_FICTIONPRESS_COMMUNITY);
		}

		private int mActiveLoaderId;
		private LoaderAdapter<BrowseMenuItem> mLoaderOff, mLoaderOn;
		private ToggleButton mToggle;

		@Override
		public void onActivityCreated(Bundle savedInstanceState) {
			super.onActivityCreated(savedInstanceState);

			Uri uri = requireActivity().getIntent().getData();
			int siteId = URI_MATCHER.match(uri);

			switch (siteId) {
			case SITE_ARCHIVE_OF_OUR_OWN:
				setTitle(R.string.menu_browse_title_stories);
				setSubTitle(Sites.ARCHIVE_OF_OUR_OWN.TITLE);
				mLoaderOff = args -> new ArchiveOfOurOwnBrowseLoader(getActivity(), args);
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					Intent i = new Intent(getActivity(), CategoryMenuActivity.class);
					i.setData(getItem(position).uri);
					startActivity(i);
				});
				break;
			case SITE_FANFICTION:
				setTitle(R.string.menu_browse_title_stories);
				setSubTitle(Sites.FANFICTION.TITLE);
				mLoaderOff = args -> new FanFictionRegularBrowseLoader(getActivity(), args);
				mLoaderOn = args -> new FanFictionCrossOverBrowseLoader(getActivity(), args);
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					Intent i = new Intent(getActivity(), CategoryMenuActivity.class);
					i.setData(getItem(position).uri);
					startActivity(i);
				});
				enableToggleButton(R.string.toggle_regular, R.string.toggle_crossover, savedInstanceState);
				break;
			case SITE_FANFICTION_COMMUNITY:
				setTitle(R.string.menu_button_communities);
				setSubTitle(Sites.FANFICTION.TITLE);
				mLoaderOff = args -> new FanFictionCommunityBrowseLoader(getActivity(), args);
				mListView.setOnItemClickListener((parent, view, position, id) -> {
					Intent i;
					if (position == 0) {
						i = new Intent(getActivity(), CommunityMenuActivity.class);
					} else {
						i = new Intent(getActivity(), CategoryMenuActivity.class);
					}
					i.setData(getItem(position).uri);
					startActivity(i);
				});
				break;
			case SITE_FICTIONPRESS:
				setTitle(R.string.menu_browse_title_stories);
				setSubTitle(Sites.FICTIONPRESS.TITLE);
				mLoaderOff = args -> new FictionpressFictionPressBrowseLoader(getActivity(), args);
				mLoaderOn = args -> new FictionPressPoetryPressBrowseLoader(getActivity(), args);