/* * Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information. */ package com.fox2code.mmm.settings; import static com.fox2code.mmm.settings.SettingsActivity.RepoFragment.applyMaterial3; import static java.lang.Integer.parseInt; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.Toast; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.preference.EditTextPreference; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreferenceCompat; import androidx.preference.TwoStatePreference; import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; import com.fox2code.foxcompat.app.FoxActivity; import com.fox2code.foxcompat.view.FoxDisplay; import com.fox2code.foxcompat.view.FoxViewCompat; import com.fox2code.mmm.AppUpdateManager; import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.Constants; import com.fox2code.mmm.MainActivity; import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.R; import com.fox2code.mmm.UpdateActivity; import com.fox2code.mmm.androidacy.AndroidacyRepoData; import com.fox2code.mmm.background.BackgroundUpdateChecker; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.module.ActionButtonType; import com.fox2code.mmm.repo.CustomRepoData; import com.fox2code.mmm.repo.CustomRepoManager; import com.fox2code.mmm.repo.RepoData; import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.utils.ExternalHelper; import com.fox2code.mmm.utils.IntentHelper; import com.fox2code.mmm.utils.ProcessHelper; import com.fox2code.mmm.utils.io.net.Http; import com.fox2code.mmm.utils.realm.ReposList; import com.fox2code.mmm.utils.sentry.SentryMain; import com.fox2code.rosettax.LanguageActivity; import com.fox2code.rosettax.LanguageSwitcher; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.navigation.NavigationBarView; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textview.MaterialTextView; import com.mikepenz.aboutlibraries.LibsBuilder; import com.topjohnwu.superuser.internal.UiThreadHandler; import org.apache.commons.io.FileUtils; import org.matomo.sdk.extra.TrackHelper; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Objects; import java.util.Random; import java.util.Set; import io.realm.Realm; import io.realm.RealmConfiguration; import io.realm.RealmResults; import timber.log.Timber; public class SettingsActivity extends FoxActivity implements LanguageActivity { // Shamelessly adapted from https://github.com/DrKLO/Telegram/blob/2c71f6c92b45386f0c2b25f1442596462404bb39/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java#L1254 public final static int PERFORMANCE_CLASS_LOW = 0; public final static int PERFORMANCE_CLASS_AVERAGE = 1; public final static int PERFORMANCE_CLASS_HIGH = 2; private static final int LANGUAGE_SUPPORT_LEVEL = 1; private static boolean devModeStepFirstBootIgnore = MainApplication.Companion.isDeveloper(); private static int devModeStep = 0; @SuppressLint("RestrictedApi") private final NavigationBarView.OnItemSelectedListener onItemSelectedListener = item -> { int itemId = item.getItemId(); if (itemId == R.id.back) { TrackHelper.track().event("view_list", "main_modules").with(Objects.requireNonNull(MainApplication.getINSTANCE()).getTracker()); startActivity(new Intent(this, MainActivity.class)); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); finish(); return true; } else //noinspection RedundantIfStatement if (itemId == R.id.settings_menu_item) { return true; } return false; }; @PerformanceClass public static int getDevicePerformanceClass() { int devicePerformanceClass; int androidVersion = Build.VERSION.SDK_INT; int cpuCount = Runtime.getRuntime().availableProcessors(); int memoryClass = ((ActivityManager) Objects.requireNonNull(MainApplication.getINSTANCE()).getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); int totalCpuFreq = 0; int freqResolved = 0; for (int i = 0; i < cpuCount; i++) { try (RandomAccessFile reader = new RandomAccessFile(String.format(Locale.ENGLISH, "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i), "r")) { String line = reader.readLine(); if (line != null) { totalCpuFreq += parseInt(line) / 1000; freqResolved++; } } catch (Exception ignore) { } } int maxCpuFreq = freqResolved == 0 ? -1 : (int) Math.ceil(totalCpuFreq / (float) freqResolved); if (androidVersion < 21 || cpuCount <= 2 || memoryClass <= 100 || cpuCount <= 4 && maxCpuFreq != -1 && maxCpuFreq <= 1250 || cpuCount <= 4 && maxCpuFreq <= 1600 && memoryClass <= 128 && androidVersion == 21 || cpuCount <= 4 && maxCpuFreq <= 1300 && memoryClass <= 128 && androidVersion <= 24) { devicePerformanceClass = PERFORMANCE_CLASS_LOW; } else if (cpuCount < 8 || memoryClass <= 160 || maxCpuFreq != -1 && maxCpuFreq <= 2050 || maxCpuFreq == -1 && cpuCount == 8 && androidVersion <= 23) { devicePerformanceClass = PERFORMANCE_CLASS_AVERAGE; } else { devicePerformanceClass = PERFORMANCE_CLASS_HIGH; } Timber.d("getDevicePerformanceClass: androidVersion=" + androidVersion + " cpuCount=" + cpuCount + " memoryClass=" + memoryClass + " maxCpuFreq=" + maxCpuFreq + " devicePerformanceClass=" + devicePerformanceClass); return devicePerformanceClass; } @SuppressLint("RestrictedApi") @Override protected void onCreate(Bundle savedInstanceState) { devModeStep = 0; super.onCreate(savedInstanceState); TrackHelper.track().screen(this).with(Objects.requireNonNull(MainApplication.getINSTANCE()).getTracker()); setContentView(R.layout.settings_activity); setTitle(R.string.app_name); //hideActionBar(); BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation); bottomNavigationView.setOnItemSelectedListener(onItemSelectedListener); if (savedInstanceState == null) { SettingsFragment settingsFragment = new SettingsFragment(); getSupportFragmentManager().beginTransaction().replace(R.id.settings, settingsFragment).commit(); } } @Override @SuppressLint("InlinedApi") public void refreshRosettaX() { ProcessHelper.restartApplicationProcess(this); } @Override protected void onPause() { BackgroundUpdateChecker.onMainActivityResume(this); super.onPause(); } public @interface PerformanceClass { } public static class SettingsFragment extends PreferenceFragmentCompat implements FoxActivity.OnBackPressedCallback { @SuppressLint("UnspecifiedImmutableFlag") @Override @SuppressWarnings("ConstantConditions") public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { String name = "mmmx"; Context context = MainApplication.getINSTANCE(); MasterKey masterKey; PreferenceManager preferenceManager = getPreferenceManager(); SharedPreferenceDataStore dataStore; SharedPreferences.Editor editor; try { masterKey = new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(); dataStore = new SharedPreferenceDataStore(EncryptedSharedPreferences.create(context, name, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)); preferenceManager.setPreferenceDataStore(dataStore); preferenceManager.setSharedPreferencesName("mmm"); editor = dataStore.getSharedPreferences().edit(); } catch (Exception e) { Timber.e(e, "Failed to create encrypted shared preferences"); throw new RuntimeException(getString(R.string.error_encrypted_shared_preferences)); } assert preferenceManager != null; setPreferencesFromResource(R.xml.root_preferences, rootKey); applyMaterial3(getPreferenceScreen()); // track all non empty values SharedPreferences sharedPreferences = dataStore.getSharedPreferences(); // disabled until EncryptedSharedPreferences fixes getAll() // add bottom navigation bar to the settings BottomNavigationView bottomNavigationView = requireActivity().findViewById(R.id.bottom_navigation); if (bottomNavigationView != null) { bottomNavigationView.setVisibility(View.VISIBLE); bottomNavigationView.getMenu().findItem(R.id.settings_menu_item).setChecked(true); } findPreference("pref_manage_repos").setOnPreferenceClickListener(p -> { devModeStep = 0; openFragment(new RepoFragment(), R.string.manage_repos_pref); return true; }); ListPreference themePreference = findPreference("pref_theme"); // If transparent theme(s) are set, disable monet if (themePreference.getValue().equals("transparent_light")) { Timber.d("disabling monet"); findPreference("pref_enable_monet").setEnabled(false); // Toggle monet off ((TwoStatePreference) findPreference("pref_enable_monet")).setChecked(false); editor.putBoolean("pref_enable_monet", false).apply(); // Set summary findPreference("pref_enable_monet").setSummary(R.string.monet_disabled_summary); // Same for blur findPreference("pref_enable_blur").setEnabled(false); ((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(false); editor.putBoolean("pref_enable_blur", false).apply(); findPreference("pref_enable_blur").setSummary(R.string.blur_disabled_summary); } themePreference.setSummaryProvider(p -> themePreference.getEntry()); themePreference.setOnPreferenceChangeListener((preference, newValue) -> { // You need to reboot your device at least once to be able to access dev-mode if (devModeStepFirstBootIgnore || !MainApplication.isFirstBoot()) devModeStep = 1; Timber.d("refreshing activity. New value: %s", newValue); editor.putString("pref_theme", (String) newValue).apply(); // If theme contains "transparent" then disable monet if (newValue.toString().contains("transparent")) { Timber.d("disabling monet"); // Show a dialogue warning the user about issues with transparent themes and // that blur/monet will be disabled new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.transparent_theme_dialogue_title).setMessage(R.string.transparent_theme_dialogue_message).setPositiveButton(R.string.ok, (dialog, which) -> { // Toggle monet off ((TwoStatePreference) findPreference("pref_enable_monet")).setChecked(false); editor.putBoolean("pref_enable_monet", false).apply(); // Set summary findPreference("pref_enable_monet").setSummary(R.string.monet_disabled_summary); // Same for blur ((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(false); editor.putBoolean("pref_enable_blur", false).apply(); findPreference("pref_enable_blur").setSummary(R.string.blur_disabled_summary); // Refresh activity devModeStep = 0; UiThreadHandler.handler.postDelayed(() -> { MainApplication.getINSTANCE().updateTheme(); FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId()); }, 1); Intent intent = new Intent(requireContext(), SettingsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); }).setNegativeButton(R.string.cancel, (dialog, which) -> { // Revert to system theme ((ListPreference) findPreference("pref_theme")).setValue("system"); // Refresh activity devModeStep = 0; }).show(); } else { findPreference("pref_enable_monet").setEnabled(true); findPreference("pref_enable_monet").setSummary(null); findPreference("pref_enable_blur").setEnabled(true); findPreference("pref_enable_blur").setSummary(null); devModeStep = 0; } UiThreadHandler.handler.postDelayed(() -> { MainApplication.getINSTANCE().updateTheme(); FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId()); }, 1); return true; }); // Crash reporting TwoStatePreference crashReportingPreference = findPreference("pref_crash_reporting"); if (!SentryMain.IS_SENTRY_INSTALLED) crashReportingPreference.setVisible(false); crashReportingPreference.setChecked(MainApplication.Companion.isCrashReportingEnabled()); final Object initialValue = MainApplication.Companion.isCrashReportingEnabled(); crashReportingPreference.setOnPreferenceChangeListener((preference, newValue) -> { devModeStepFirstBootIgnore = true; devModeStep = 0; if (initialValue == newValue) return true; // Show a dialog to restart the app MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(requireContext()); materialAlertDialogBuilder.setTitle(R.string.crash_reporting_restart_title); materialAlertDialogBuilder.setMessage(R.string.crash_reporting_restart_message); materialAlertDialogBuilder.setPositiveButton(R.string.restart, (dialog, which) -> { Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; // If < 23, FLAG_IMMUTABLE is not available PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save crash reporting preference: %s", newValue); System.exit(0); // Exit app process }); // Do not reverse the change if the user cancels the dialog materialAlertDialogBuilder.setNegativeButton(R.string.no, (dialog, which) -> { }); materialAlertDialogBuilder.show(); return true; }); Preference enableBlur = findPreference("pref_enable_blur"); // Disable blur on low performance devices if (getDevicePerformanceClass() < PERFORMANCE_CLASS_AVERAGE) { // Show a warning enableBlur.setOnPreferenceChangeListener((preference, newValue) -> { if (newValue.equals(true)) { new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.low_performance_device_dialogue_title).setMessage(R.string.low_performance_device_dialogue_message).setPositiveButton(R.string.ok, (dialog, which) -> { // Toggle blur on ((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(true); editor.putBoolean("pref_enable_blur", true).apply(); // Set summary findPreference("pref_enable_blur").setSummary(R.string.blur_disabled_summary); }).setNegativeButton(R.string.cancel, (dialog, which) -> { // Revert to blur on ((TwoStatePreference) findPreference("pref_enable_blur")).setChecked(false); editor.putBoolean("pref_enable_blur", false).apply(); // Set summary findPreference("pref_enable_blur").setSummary(null); }).show(); } return true; }); } Preference disableMonet = findPreference("pref_enable_monet"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { disableMonet.setSummary(R.string.require_android_12); disableMonet.setEnabled(false); } disableMonet.setOnPreferenceClickListener(preference -> { UiThreadHandler.handler.postDelayed(() -> { MainApplication.getINSTANCE().updateTheme(); ((FoxActivity) this.requireActivity()).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId()); }, 1); return true; }); findPreference("pref_dns_over_https").setOnPreferenceChangeListener((p, v) -> { Http.setDoh((Boolean) v); return true; }); // handle restart required for showcase mode findPreference("pref_showcase_mode").setOnPreferenceChangeListener((p, v) -> { if (v.equals(true)) { new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.restart).setMessage(R.string.showcase_mode_dialogue_message).setPositiveButton(R.string.ok, (dialog, which) -> { // Toggle showcase mode on ((TwoStatePreference) findPreference("pref_showcase_mode")).setChecked(true); editor.putBoolean("pref_showcase_mode", true).apply(); // restart app Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save showcase mode preference: %s", v); System.exit(0); // Exit app process }).setNegativeButton(R.string.cancel, (dialog, which) -> { // Revert to showcase mode on ((TwoStatePreference) findPreference("pref_showcase_mode")).setChecked(false); editor.putBoolean("pref_showcase_mode", false).apply(); // restart app Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save showcase mode preference: %s", v); System.exit(0); // Exit app process }).show(); } return true; }); Preference languageSelector = findPreference("pref_language_selector"); languageSelector.setOnPreferenceClickListener(preference -> { LanguageSwitcher ls = new LanguageSwitcher(getActivity()); ls.setSupportedStringLocales(MainApplication.supportedLocales); ls.showChangeLanguageDialog(getActivity()); return true; }); // Handle pref_language_selector_cta by taking user to https://translate.nift4.org/engage/foxmmm/ LongClickablePreference languageSelectorCta = findPreference("pref_language_selector_cta"); languageSelectorCta.setOnPreferenceClickListener(preference -> { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://translate.nift4.org/engage/foxmmm/")); startActivity(browserIntent); return true; }); // Long click to copy url languageSelectorCta.setOnPreferenceLongClickListener(v -> { ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("URL", "https://translate.nift4.org/engage/foxmmm/"); clipboard.setPrimaryClip(clip); Toast.makeText(requireContext(), R.string.link_copied, Toast.LENGTH_SHORT).show(); return true; }); int level = this.currentLanguageLevel(); if (level != LANGUAGE_SUPPORT_LEVEL) { Timber.e("latest is %s", LANGUAGE_SUPPORT_LEVEL); languageSelector.setSummary(R.string.language_support_outdated); } else { String translatedBy = this.getString(R.string.language_translated_by); // I don't "translate" english if (!("Translated by Fox2Code (Put your name here)".equals(translatedBy) || "Translated by Fox2Code".equals(translatedBy))) { languageSelector.setSummary(R.string.language_translated_by); } else { languageSelector.setSummary(null); } } if (!MainApplication.isDeveloper()) { findPreference("pref_disable_low_quality_module_filter").setVisible(false); // Find pref_clear_data and set it invisible Objects.requireNonNull((Preference) findPreference("pref_clear_data")).setVisible(false); } // hande clear cache findPreference("pref_clear_cache").setOnPreferenceClickListener(preference -> { // Clear cache new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.clear_cache_dialogue_title).setMessage(R.string.clear_cache_dialogue_message).setPositiveButton(R.string.yes, (dialog, which) -> { // Clear app cache try { // use apache commons IO to delete the cache FileUtils.deleteDirectory(requireContext().getCacheDir()); // create a new cache dir FileUtils.forceMkdir(requireContext().getCacheDir()); // create cache dirs for cronet and webview FileUtils.forceMkdir(new File(requireContext().getCacheDir(), "cronet")); FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm")); FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js")); Toast.makeText(requireContext(), R.string.cache_cleared, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Timber.e(e); Toast.makeText(requireContext(), R.string.cache_clear_failed, Toast.LENGTH_SHORT).show(); } }).setNegativeButton(R.string.no, (dialog, which) -> { // Do nothing }).show(); return true; }); if (!SentryMain.IS_SENTRY_INSTALLED || !BuildConfig.DEBUG || InstallerInitializer.peekMagiskPath() == null) { // Hide the pref_crash option if not in debug mode - stop users from purposely crashing the app Timber.i(InstallerInitializer.peekMagiskPath()); Objects.requireNonNull((Preference) findPreference("pref_test_crash")).setVisible(false); } else { if (findPreference("pref_test_crash") != null && findPreference("pref_clear_data") != null) { findPreference("pref_test_crash").setOnPreferenceClickListener(preference -> { // Hard crash the app // we need a stacktrace to see if the crash is from the app or from the system throw new RuntimeException("This is a test crash with a stupidly long description to show off the crash handler. Are we having fun yet?"); }); findPreference("pref_clear_data").setOnPreferenceClickListener(preference -> { // Clear app data new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.clear_data_dialogue_title).setMessage(R.string.clear_data_dialogue_message).setPositiveButton(R.string.yes, (dialog, which) -> { // Clear app data MainApplication.getINSTANCE().resetApp(); }).setNegativeButton(R.string.no, (dialog, which) -> { }).show(); return true; }); } else { Timber.e("Something is null: %s, %s", findPreference("pref_clear_data"), findPreference("pref_test_crash")); } } if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND || !MainApplication.isDeveloper()) { findPreference("pref_use_magisk_install_command").setVisible(false); } Preference debugNotification = findPreference("pref_background_update_check_debug"); Preference updateCheckExcludes = findPreference("pref_background_update_check_excludes"); Preference updateCheckVersionExcludes = findPreference("pref_background_update_check_excludes_version"); debugNotification.setEnabled(MainApplication.Companion.isBackgroundUpdateCheckEnabled()); debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped && MainApplication.Companion.isBackgroundUpdateCheckEnabled()); debugNotification.setOnPreferenceClickListener(preference -> { // fake updatable modules hashmap HashMap updateableModules = new HashMap<>(); // count of modules to fake must match the count in the random number generator Random random = new Random(); int count; do { count = random.nextInt(4) + 2; } while (count == 2); for (int i = 0; i < count; i++) { int fakeVersion; do { fakeVersion = random.nextInt(10); } while (fakeVersion == 0); Timber.d("Fake version: %s, count: %s", fakeVersion, i); updateableModules.put("FakeModule " + i, "1.0." + fakeVersion); } BackgroundUpdateChecker.postNotification(this.requireContext(), updateableModules, count, true); return true; }); Preference backgroundUpdateCheck = findPreference("pref_background_update_check"); backgroundUpdateCheck.setVisible(!MainApplication.isWrapped); // Make uncheckable if POST_NOTIFICATIONS permission is not granted if (!MainApplication.isNotificationPermissionGranted()) { // Instead of disabling the preference, we make it uncheckable and when the user // clicks on it, we show a dialog explaining why the permission is needed backgroundUpdateCheck.setOnPreferenceClickListener(preference -> { // set the box to unchecked ((SwitchPreferenceCompat) backgroundUpdateCheck).setChecked(false); // ensure that the preference is false MainApplication.getSharedPreferences("mmm").edit().putBoolean("pref_background_update_check", false).apply(); new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.permission_notification_title).setMessage(R.string.permission_notification_message).setPositiveButton(R.string.ok, (dialog, which) -> { // Open the app settings Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", this.requireContext().getPackageName(), null); intent.setData(uri); this.startActivity(intent); }).setNegativeButton(R.string.cancel, (dialog, which) -> { }).show(); return true; }); backgroundUpdateCheck.setSummary(R.string.background_update_check_permission_required); } updateCheckExcludes.setVisible(MainApplication.Companion.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped); backgroundUpdateCheck.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = Boolean.parseBoolean(String.valueOf(newValue)); debugNotification.setEnabled(enabled); debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped && enabled); updateCheckExcludes.setEnabled(enabled); updateCheckExcludes.setVisible(enabled && !MainApplication.isWrapped); if (!enabled) { BackgroundUpdateChecker.onMainActivityResume(this.requireContext()); } return true; }); // updateCheckExcludes saves to pref_background_update_check_excludes as a stringset. On clicking, it should open a dialog with a list of all installed modules updateCheckExcludes.setOnPreferenceClickListener(preference -> { Collection localModuleInfos = ModuleManager.getInstance().getModules().values(); // make sure we have modules boolean[] checkedItems; if (!localModuleInfos.isEmpty()) { String[] moduleNames = new String[localModuleInfos.size()]; checkedItems = new boolean[localModuleInfos.size()]; int i = 0; // get the stringset pref_background_update_check_excludes Set stringSetTemp = sharedPreferences.getStringSet("pref_background_update_check_excludes", new HashSet<>()); // copy to a new set so we can modify it Set stringSet = new HashSet<>(stringSetTemp); for (LocalModuleInfo localModuleInfo : localModuleInfos) { moduleNames[i] = localModuleInfo.name; // Stringset uses id, we show name checkedItems[i] = stringSet.contains(localModuleInfo.id); Timber.d("name: %s, checked: %s", moduleNames[i], checkedItems[i]); i++; } new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMultiChoiceItems(moduleNames, checkedItems, (dialog, which, isChecked) -> { // get id from name String id; if (localModuleInfos.stream().anyMatch(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which]))) { id = localModuleInfos.stream().filter(localModuleInfo -> localModuleInfo.name.equals(moduleNames[which])).findFirst().orElse(null).id; } else { id = ""; } if (!id.isEmpty()) { if (isChecked) { stringSet.add(id); } else { stringSet.remove(id); } } sharedPreferences.edit().putStringSet("pref_background_update_check_excludes", stringSet).apply(); }).setPositiveButton(R.string.ok, (dialog, which) -> { }).show(); } else { new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMessage(R.string.background_update_check_excludes_no_modules).setPositiveButton(R.string.ok, (dialog, which) -> { }).show(); } return true; }); // now handle pref_background_update_check_excludes_version updateCheckVersionExcludes.setVisible(MainApplication.Companion.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped); updateCheckVersionExcludes.setOnPreferenceClickListener(preference -> { // get the stringset pref_background_update_check_excludes_version Set stringSet = sharedPreferences.getStringSet("pref_background_update_check_excludes_version", new HashSet<>()); Timber.d("stringSet: %s", stringSet); // for every module, add it's name and a text field to the dialog. the text field should accept a comma separated list of versions Collection localModuleInfos = ModuleManager.getInstance().getModules().values(); // make sure we have modules if (localModuleInfos.isEmpty()) { new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes).setMessage(R.string.background_update_check_excludes_no_modules).setPositiveButton(R.string.ok, (dialog, which) -> { }).show(); } else { LinearLayout layout = new LinearLayout(this.requireContext()); layout.setOrientation(LinearLayout.VERTICAL); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.setMargins(48, 0, 48, 0); // add a summary MaterialTextView textView = new MaterialTextView(this.requireContext()); textView.setLayoutParams(params); textView.setText(R.string.background_update_check_excludes_version_summary); for (LocalModuleInfo localModuleInfo : localModuleInfos) { // two views: materialtextview for name, edittext for version MaterialTextView materialTextView = new MaterialTextView(this.requireContext()); materialTextView.setLayoutParams(params); materialTextView.setPadding(12, 8, 12, 8); materialTextView.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Subtitle1); materialTextView.setText(localModuleInfo.name); layout.addView(materialTextView); EditText editText = new EditText(this.requireContext()); editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); editText.setLayoutParams(params); editText.setHint(R.string.background_update_check_excludes_version_hint); // stringset uses id:version, we show version for name // so we need to get id from name, then get version from stringset String id = localModuleInfos.stream().filter(localModuleInfo1 -> localModuleInfo1.name.equals(localModuleInfo.name)).findFirst().orElse(null).id; String version = stringSet.stream().filter(s -> s.startsWith(id)).findFirst().orElse(""); if (!version.isEmpty()) { editText.setText(version.split(":")[1]); } layout.addView(editText); } ScrollView scrollView = new ScrollView(this.requireContext()); scrollView.addView(layout); new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.background_update_check_excludes_version).setView(scrollView).setPositiveButton(R.string.ok, (dialog, which) -> { Timber.d("ok clicked"); // for every module, get the text field and save it to the stringset Set stringSetTemp = new HashSet<>(); String prevMod = ""; for (int i = 0; i < layout.getChildCount(); i++) { if (layout.getChildAt(i) instanceof MaterialTextView mv) { prevMod = mv.getText().toString(); continue; } EditText editText = (EditText) layout.getChildAt(i); String text = editText.getText().toString(); if (!text.isEmpty()) { // text can only contain numbers and the characters ^ and $ // so we remove all non-numbers and non ^ and $ text = text.replaceAll("[^0-9^$]", ""); // we have to use module id even though we show name String finalprevMod = prevMod; stringSetTemp.add(localModuleInfos.stream().filter(localModuleInfo -> localModuleInfo.name.equals(finalprevMod)).findFirst().orElse(null).id + ":" + text); Timber.d("text is %s for %s", text, editText.getHint().toString()); } else { Timber.d("text is empty for %s", editText.getHint().toString()); } } sharedPreferences.edit().putStringSet("pref_background_update_check_excludes_version", stringSetTemp).apply(); }).setNegativeButton(R.string.cancel, (dialog, which) -> { }).show(); } return true; }); final LibsBuilder libsBuilder = new LibsBuilder().withShowLoadingProgress(false).withLicenseShown(true).withAboutMinimalDesign(false); ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE); LongClickablePreference linkClickable = findPreference("pref_update"); linkClickable.setVisible(BuildConfig.ENABLE_AUTO_UPDATER && (BuildConfig.DEBUG || AppUpdateManager.Companion.getAppUpdateManager().peekHasUpdate())); linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; // open UpdateActivity with CHECK action Intent intent = new Intent(requireContext(), UpdateActivity.class); intent.setAction(UpdateActivity.ACTIONS.CHECK.name()); startActivity(intent); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Androidacy/MagiskModuleManager/releases/latest")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); // for pref_background_update_check_debug_download, do the same as pref_update except with DOWNLOAD action Preference debugDownload = findPreference("pref_background_update_check_debug_download"); debugDownload.setVisible(MainApplication.isDeveloper() && MainApplication.Companion.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped); debugDownload.setOnPreferenceClickListener(p -> { devModeStep = 0; Intent intent = new Intent(requireContext(), UpdateActivity.class); intent.setAction(UpdateActivity.ACTIONS.DOWNLOAD.name()); startActivity(intent); return true; }); if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) { linkClickable = findPreference("pref_report_bug"); linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; devModeStepFirstBootIgnore = true; IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Androidacy/MagiskModuleManager/issues"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Androidacy/MagiskModuleManager/issues")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { findPreference("pref_report_bug").setVisible(false); } linkClickable = findPreference("pref_source_code"); // Set summary to the last commit this build was built from @ User/Repo // Build userRepo by removing all parts of REMOTE_URL that are not the user/repo String userRepo = BuildConfig.REMOTE_URL; // remove .git userRepo = userRepo.replaceAll("\\.git$", ""); Timber.d("userRepo: %s", userRepo); // finalUserRepo is the user/repo part of REMOTE_URL // get everything after .com/ or .org/ or .io/ or .me/ or .net/ or .xyz/ or .tk/ or .co/ minus .git String finalUserRepo = userRepo.replaceAll("^(https?://)?(www\\.)?(github\\.com|gitlab\\.com|bitbucket\\.org|git\\.io|git\\.me|git\\.net|git\\.xyz|git\\.tk|git\\.co)/", ""); linkClickable.setSummary(String.format(getString(R.string.source_code_summary), BuildConfig.COMMIT_HASH, finalUserRepo)); Timber.d("finalUserRepo: %s", finalUserRepo); String finalUserRepo1 = userRepo; linkClickable.setOnPreferenceClickListener(p -> { if (devModeStep == 2) { devModeStep = 0; if (MainApplication.isDeveloper() && !BuildConfig.DEBUG) { MainApplication.getSharedPreferences("mmm").edit().putBoolean("developer", false).apply(); Toast.makeText(getContext(), // Tell the user something changed R.string.dev_mode_disabled, Toast.LENGTH_SHORT).show(); } else { MainApplication.getSharedPreferences("mmm").edit().putBoolean("developer", true).apply(); Toast.makeText(getContext(), // Tell the user something changed R.string.dev_mode_enabled, Toast.LENGTH_SHORT).show(); } ExternalHelper.INSTANCE.refreshHelper(getContext()); return true; } // build url from BuildConfig.REMOTE_URL and BuildConfig.COMMIT_HASH. May have to remove the .git at the end IntentHelper.Companion.openUrl(p.getContext(), finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, BuildConfig.REMOTE_URL + "/tree/" + BuildConfig.COMMIT_HASH)); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); // Next, the pref_androidacy_thanks should lead to the androidacy website linkClickable = findPreference("pref_androidacy_thanks"); linkClickable.setOnPreferenceClickListener(p -> { IntentHelper.Companion.openUrl(p.getContext(), "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); // pref_fox2code_thanks should lead to https://github.com/Fox2Code linkClickable = findPreference("pref_fox2code_thanks"); linkClickable.setOnPreferenceClickListener(p -> { IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Fox2Code"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Fox2Code")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); // handle pref_save_logs which saves logs to our external storage and shares them Preference saveLogs = findPreference("pref_save_logs"); saveLogs.setOnPreferenceClickListener(p -> { // Save logs to external storage File logsFile = new File(requireContext().getExternalFilesDir(null), "logs.txt"); FileOutputStream fileOutputStream = null; try { //noinspection ResultOfMethodCallIgnored logsFile.createNewFile(); fileOutputStream = new FileOutputStream(logsFile); // first, write some info about the device fileOutputStream.write(("FoxMagiskModuleManager version: " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")\n").getBytes()); fileOutputStream.write(("Android version: " + Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")\n").getBytes()); fileOutputStream.write(("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")\n").getBytes()); fileOutputStream.write(("Magisk version: " + InstallerInitializer.peekMagiskVersion() + "\n").getBytes()); fileOutputStream.write(("Has internet: " + (RepoManager.getINSTANCE().hasConnectivity() ? "Yes" : "No") + "\n").getBytes()); // read our logcat but format the output to be more readable Process process = Runtime.getRuntime().exec("logcat -d -v tag"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = bufferedReader.readLine()) != null) { fileOutputStream.write((line + "\n").getBytes()); } } catch (IOException e) { e.printStackTrace(); Toast.makeText(requireContext(), R.string.error_saving_logs, Toast.LENGTH_SHORT).show(); return true; } finally { if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException ignored) { } } } // Share logs Intent shareIntent = new Intent(); // create a new intent and grantUriPermission to the file provider shareIntent.setAction(Intent.ACTION_SEND); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); shareIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(requireContext(), BuildConfig.APPLICATION_ID + ".file-provider", logsFile)); shareIntent.setType("text/plain"); startActivity(Intent.createChooser(shareIntent, getString(R.string.share_logs))); return true; }); // pref_contributors should lead to the contributors page linkClickable = findPreference("pref_contributors"); linkClickable.setOnPreferenceClickListener(p -> { // Remove the .git if it exists and add /graphs/contributors String url = BuildConfig.REMOTE_URL; if (url.endsWith(".git")) { url = url.substring(0, url.length() - 4); } url += "/graphs/contributors"; IntentHelper.Companion.openUrl(p.getContext(), url); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); // Remove the .git if it exists and add /graphs/contributors String url = BuildConfig.REMOTE_URL; if (url.endsWith(".git")) { url = url.substring(0, url.length() - 4); } url += "/graphs/contributors"; clipboard.setPrimaryClip(ClipData.newPlainText(toastText, url)); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); linkClickable = findPreference("pref_support"); linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/Fox2Code_Chat"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://t.me/Fox2Code_Chat")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); // pref_announcements to https://t.me/androidacy linkClickable = findPreference("pref_announcements"); linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/androidacy"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://t.me/androidacy")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); findPreference("pref_show_licenses").setOnPreferenceClickListener(p -> { devModeStep = devModeStep == 1 ? 2 : 0; BackgroundUpdateChecker.onMainActivityResume(this.requireContext()); openFragment(libsBuilder.supportFragment(), R.string.licenses); return true; }); // Determine if this is an official build based on the signature String flavor = BuildConfig.FLAVOR; String type = BuildConfig.BUILD_TYPE; // Set the summary of pref_pkg_info to something like default-debug v1.0 (123) (Official) String pkgInfo = getString(R.string.pref_pkg_info_summary, flavor + "-" + type, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, MainApplication.o ? getString(R.string.official) : getString(R.string.unofficial)); findPreference("pref_pkg_info").setSummary(pkgInfo); // special easter egg :) var ref = new Object() { int versionClicks = 0; }; findPreference("pref_pkg_info").setOnPreferenceClickListener(p -> { ref.versionClicks++; Timber.d("Version clicks: %d", ref.versionClicks); // if it's been 3 clicks, toast "yer a wizard, harry" or "keep tapping to enter hogwarts" if (ref.versionClicks == 3) { // random choice of 1 or 2 Random rand = new Random(); int n = rand.nextInt(2) + 1; Toast.makeText(p.getContext(), n == 1 ? R.string.yer_a_wizard_harry : R.string.keep_tapping_to_enter_hogwarts, Toast.LENGTH_SHORT).show(); } if (ref.versionClicks == 7) { ref.versionClicks = 0; IntentHelper.Companion.openUrl(p.getContext(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } return true; }); LongClickablePreference pref_donate_fox = findPreference("pref_donate_fox"); if (!BuildConfig.FLAVOR.equals("play")) { pref_donate_fox.setOnPreferenceClickListener(p -> { // open fox IntentHelper.Companion.openUrl(getFoxActivity(this), "https://paypal.me/fox2code"); return true; }); // handle long click on pref_donate_fox pref_donate_fox.setOnPreferenceLongClickListener(p -> { // copy to clipboard String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://paypal.me/fox2code")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { pref_donate_fox.setVisible(false); } // now handle pref_donate_androidacy LongClickablePreference pref_donate_androidacy = findPreference("pref_donate_androidacy"); if (!BuildConfig.FLAVOR.equals("play")) { if (AndroidacyRepoData.getInstance().isEnabled() && Objects.equals(AndroidacyRepoData.getInstance().memberLevel, "Guest") || AndroidacyRepoData.getInstance().memberLevel == null) { pref_donate_androidacy.setOnPreferenceClickListener(p -> { // copy FOX2CODE promo code to clipboard and toast user that they can use it for half off any subscription String toastText = requireContext().getString(R.string.promo_code_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "FOX2CODE")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); // open androidacy IntentHelper.Companion.openUrl(getFoxActivity(this), "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate"); return true; }); // handle long click on pref_donate_androidacy pref_donate_androidacy.setOnPreferenceLongClickListener(p -> { // copy to clipboard String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { // set text to "Thank you for your support!" pref_donate_androidacy.setSummary(R.string.androidacy_thanks_up); pref_donate_androidacy.setTitle(R.string.androidacy_thanks_up_title); } } else { pref_donate_androidacy.setVisible(false); } } private void openFragment(Fragment fragment, @StringRes int title) { FoxActivity compatActivity = getFoxActivity(this); compatActivity.setOnBackPressedCallback(this); compatActivity.setTitle(title); compatActivity.getSupportFragmentManager().beginTransaction().replace(R.id.settings, fragment).setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE).commit(); } @Override public boolean onBackPressed(FoxActivity compatActivity) { compatActivity.setTitle(R.string.app_name); compatActivity.getSupportFragmentManager().beginTransaction().replace(R.id.settings, this).setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE).commit(); return true; } private int currentLanguageLevel() { int declaredLanguageLevel = this.getResources().getInteger(R.integer.language_support_level); if (declaredLanguageLevel != LANGUAGE_SUPPORT_LEVEL) return declaredLanguageLevel; if (!this.getResources().getConfiguration().getLocales().get(0).getLanguage().equals("en") && this.getResources().getString(R.string.notification_update_pref).equals("Background modules update check") && this.getResources().getString(R.string.notification_update_desc).equals("May increase battery usage")) { return 0; } return LANGUAGE_SUPPORT_LEVEL; } } @SuppressWarnings("ConstantConditions") public static class RepoFragment extends PreferenceFragmentCompat { /** * says proudly: I stole it *

* namely, from neo wellbeing */ public static void applyMaterial3(Preference p) { if (p instanceof PreferenceGroup pg) { for (int i = 0; i < pg.getPreferenceCount(); i++) { applyMaterial3(pg.getPreference(i)); } } if (p instanceof SwitchPreferenceCompat) { p.setWidgetLayoutResource(R.layout.preference_material_switch); } } @SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"}) public void onCreatePreferencesAndroidacy() { // Bind the pref_show_captcha_webview to captchaWebview('https://production-api.androidacy.com/') // Also require dev mode // CaptchaWebview.setVisible(false); Preference androidacyTestMode = Objects.requireNonNull(findPreference("pref_androidacy_test_mode")); if (!MainApplication.isDeveloper()) { androidacyTestMode.setVisible(false); } else { // Show a warning if user tries to enable test mode androidacyTestMode.setOnPreferenceChangeListener((preference, newValue) -> { if (Boolean.parseBoolean(String.valueOf(newValue))) { // Use MaterialAlertDialogBuilder new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.warning).setCancelable(false).setMessage(R.string.androidacy_test_mode_warning).setPositiveButton(android.R.string.ok, (dialog, which) -> { // User clicked OK button MainApplication.getSharedPreferences("mmm").edit().putBoolean("androidacy_test_mode", true).apply(); // Check the switch Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; // If < 23, FLAG_IMMUTABLE is not available PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save staging endpoint preference: %s", newValue); System.exit(0); // Exit app process }).setNegativeButton(android.R.string.cancel, (dialog, which) -> { // User cancelled the dialog // Uncheck the switch SwitchPreferenceCompat switchPreferenceCompat = (SwitchPreferenceCompat) androidacyTestMode; switchPreferenceCompat.setChecked(false); // There's probably a better way to do this than duplicate code but I'm too lazy to figure it out MainApplication.getSharedPreferences("mmm").edit().putBoolean("androidacy_test_mode", false).apply(); }).show(); } else { MainApplication.getSharedPreferences("mmm").edit().putBoolean("androidacy_test_mode", false).apply(); // Show dialog to restart app with ok button new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.warning).setCancelable(false).setMessage(R.string.androidacy_test_mode_disable_warning).setNeutralButton(android.R.string.ok, (dialog, which) -> { // User clicked OK button Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; // If < 23, FLAG_IMMUTABLE is not available PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save staging endpoint preference: %s", newValue); System.exit(0); // Exit app process }).show(); } return true; }); } // Get magisk_alt_repo enabled state from realm db RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm1 = Realm.getInstance(realmConfig); ReposList reposList = realm1.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst(); if (reposList != null) { // Set the switch to the current state SwitchPreferenceCompat magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled")); magiskAltRepoEnabled.setChecked(reposList.isEnabled()); } // add listener to magisk_alt_repo_enabled switch to update realm db Preference magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled")); magiskAltRepoEnabled.setOnPreferenceChangeListener((preference, newValue) -> { // Update realm db Realm realm = Realm.getInstance(realmConfig); realm.executeTransaction(realm2 -> { ReposList reposList1 = realm2.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst(); if (reposList1 != null) { reposList1.setEnabled(Boolean.parseBoolean(String.valueOf(newValue))); } else { Timber.e("Alt Repo not found in realm db"); } }); return true; }); // Disable toggling the pref_androidacy_repo_enabled on builds without an // ANDROIDACY_CLIENT_ID or where the ANDROIDACY_CLIENT_ID is empty SwitchPreferenceCompat androidacyRepoEnabled = Objects.requireNonNull(findPreference("pref_androidacy_repo_enabled")); if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) { androidacyRepoEnabled.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.androidacy_repo_disabled).setCancelable(false).setMessage(R.string.androidacy_repo_disabled_message).setPositiveButton(R.string.download_full_app, (dialog, which) -> { // User clicked OK button. Open GitHub releases page Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=FoxMMM&utm_medium=app&utm_campaign=FoxMMM")); startActivity(browserIntent); }).show(); // Revert the switch to off androidacyRepoEnabled.setChecked(false); // Disable in realm db RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); realm.executeTransaction(realm2 -> { ReposList repoRealmResults = realm2.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst(); assert repoRealmResults != null; repoRealmResults.setEnabled(false); realm2.insertOrUpdate(repoRealmResults); realm2.close(); }); return false; }); } else { // get if androidacy repo is enabled from realm db RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); ReposList repoRealmResults = realm.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst(); if (repoRealmResults == null) { throw new IllegalStateException("Androidacy repo not found in realm db"); } boolean androidacyRepoEnabledPref = repoRealmResults.isEnabled(); // set the switch to the current state androidacyRepoEnabled.setChecked(androidacyRepoEnabledPref); // add a click listener to the switch androidacyRepoEnabled.setOnPreferenceClickListener(preference -> { boolean enabled = androidacyRepoEnabled.isChecked(); // save the new state realm.executeTransaction(realm2 -> { ReposList repoRealmResults1 = realm2.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst(); repoRealmResults1.setEnabled(enabled); }); return true; }); if (androidacyRepoEnabledPref) { // get user role from AndroidacyRepoData.userInfo String[][] userInfo = AndroidacyRepoData.getInstance().userInfo; if (userInfo != null) { String userRole = userInfo[0][1]; if (Objects.nonNull(userRole) && !Objects.equals(userRole, "Guest")) { // Disable the pref_androidacy_repo_api_donate preference LongClickablePreference prefAndroidacyRepoApiD = Objects.requireNonNull(findPreference("pref_androidacy_repo_donate")); prefAndroidacyRepoApiD.setEnabled(false); prefAndroidacyRepoApiD.setSummary(R.string.upgraded_summary); prefAndroidacyRepoApiD.setTitle(R.string.upgraded); prefAndroidacyRepoApiD.setIcon(R.drawable.baseline_check_24); } else if (BuildConfig.FLAVOR.equals("play")) { // Disable the pref_androidacy_repo_api_token preference and hide the donate button LongClickablePreference prefAndroidacyRepoApiD = Objects.requireNonNull(findPreference("pref_androidacy_repo_donate")); prefAndroidacyRepoApiD.setEnabled(false); prefAndroidacyRepoApiD.setVisible(false); } } String[] originalApiKeyRef = new String[]{MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", "")}; // Get the dummy pref_androidacy_repo_api_token preference with id pref_androidacy_repo_api_token // we have to use the id because the key is different EditTextPreference prefAndroidacyRepoApiKey = Objects.requireNonNull(findPreference("pref_androidacy_repo_api_token")); // add validation to the EditTextPreference // string must be 64 characters long, and only allows alphanumeric characters prefAndroidacyRepoApiKey.setTitle(R.string.api_key); prefAndroidacyRepoApiKey.setSummary(R.string.api_key_summary); prefAndroidacyRepoApiKey.setDialogTitle(R.string.api_key); prefAndroidacyRepoApiKey.setDefaultValue(originalApiKeyRef[0]); // Set the value to the current value prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]); prefAndroidacyRepoApiKey.setVisible(true); prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> { editText.setSingleLine(); // Make the single line wrap editText.setHorizontallyScrolling(false); // Set the height to the maximum required to fit the text editText.setMaxLines(Integer.MAX_VALUE); // Make ok button say "Save" editText.setImeOptions(EditorInfo.IME_ACTION_DONE); }); prefAndroidacyRepoApiKey.setPositiveButtonText(R.string.save_api_key); prefAndroidacyRepoApiKey.setOnPreferenceChangeListener((preference, newValue) -> { // validate the api key client side first. should be 64 characters long, and only allow alphanumeric characters if (!newValue.toString().matches("[a-zA-Z0-9]{64}")) { // Show snack bar with error Snackbar.make(requireView(), R.string.api_key_mismatch, BaseTransientBottomBar.LENGTH_LONG).show(); // Restore the original api key prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]); prefAndroidacyRepoApiKey.performClick(); return false; } // Make sure originalApiKeyRef is not null if (originalApiKeyRef[0].equals(newValue)) return true; // get original api key String apiKey = String.valueOf(newValue); // Show snack bar with indeterminate progress Snackbar.make(requireView(), R.string.checking_api_key, BaseTransientBottomBar.LENGTH_INDEFINITE).setAction(R.string.cancel, v -> { // Restore the original api key prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]); }).show(); // Check the API key on a background thread new Thread(() -> { // If key is empty, just remove it and change the text of the snack bar if (apiKey.isEmpty()) { MainApplication.getSharedPreferences("androidacy").edit().remove("pref_androidacy_api_token").apply(); new Handler(Looper.getMainLooper()).post(() -> { Snackbar.make(requireView(), R.string.api_key_removed, BaseTransientBottomBar.LENGTH_SHORT).show(); // Show dialog to restart app with ok button new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.restart).setCancelable(false).setMessage(R.string.api_key_restart).setNeutralButton(android.R.string.ok, (dialog, which) -> { // User clicked OK button Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; // If < 23, FLAG_IMMUTABLE is not available PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save token preference: %s", newValue); System.exit(0); // Exit app process }).show(); }); } else { // If key < 64 chars, it's not valid if (apiKey.length() < 64) { new Handler(Looper.getMainLooper()).post(() -> { Snackbar.make(requireView(), R.string.api_key_invalid, BaseTransientBottomBar.LENGTH_SHORT).show(); // Save the original key MainApplication.getSharedPreferences("androidacy").edit().putString("pref_androidacy_api_token", originalApiKeyRef[0]).apply(); // Re-show the dialog with an error prefAndroidacyRepoApiKey.performClick(); // Show error prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid)); }); } else { // If the key is the same as the original, just show a snack bar if (apiKey.equals(originalApiKeyRef[0])) { new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), R.string.api_key_unchanged, BaseTransientBottomBar.LENGTH_SHORT).show()); return; } boolean valid = false; try { valid = AndroidacyRepoData.getInstance().isValidToken(apiKey); } catch (IOException ignored) { } // If the key is valid, save it if (valid) { originalApiKeyRef[0] = apiKey; RepoManager.getINSTANCE().getAndroidacyRepoData().setToken(apiKey); MainApplication.getSharedPreferences("androidacy").edit().putString("pref_androidacy_api_token", apiKey).apply(); // Snackbar with success and restart button new Handler(Looper.getMainLooper()).post(() -> { Snackbar.make(requireView(), R.string.api_key_valid, BaseTransientBottomBar.LENGTH_SHORT).show(); // Show dialog to restart app with ok button new MaterialAlertDialogBuilder(this.requireContext()).setTitle(R.string.restart).setCancelable(false).setMessage(R.string.api_key_restart).setNeutralButton(android.R.string.ok, (dialog, which) -> { // User clicked OK button Intent mStartActivity = new Intent(requireContext(), MainActivity.class); mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); int mPendingIntentId = 123456; // If < 23, FLAG_IMMUTABLE is not available PendingIntent mPendingIntent; mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); Timber.d("Restarting app to save token preference: %s", newValue); System.exit(0); // Exit app process }).show(); }); } else { new Handler(Looper.getMainLooper()).post(() -> { Snackbar.make(requireView(), R.string.api_key_invalid, BaseTransientBottomBar.LENGTH_SHORT).show(); // Save the original key MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString("pref_androidacy_api_token", originalApiKeyRef[0]).apply(); // Re-show the dialog with an error prefAndroidacyRepoApiKey.performClick(); // Show error prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid)); }); } } } }).start(); return true; }); } } } @SuppressLint("RestrictedApi") public void updateCustomRepoList(boolean initial) { RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); // get all repos that are not built-in int CUSTOM_REPO_ENTRIES = 0; // array of custom repos ArrayList customRepos = new ArrayList<>(); RealmResults customRepoDataDB = realm.where(ReposList.class).findAll(); for (ReposList repo : customRepoDataDB) { if (!repo.getId().equals("androidacy_repo") && !repo.getId().equals("magisk_alt_repo")) { CUSTOM_REPO_ENTRIES++; customRepos.add(repo.getUrl()); } } Timber.d("%d repos: %s", CUSTOM_REPO_ENTRIES, customRepos); final CustomRepoManager customRepoManager = RepoManager.getINSTANCE().getCustomRepoManager(); for (int i = 0; i < CUSTOM_REPO_ENTRIES; i++) { // get the id of the repo at current index in customRepos CustomRepoData repoData = customRepoManager.getRepo(customRepos.get(i)); assert repoData != null; // convert repoData to a json string for logging Timber.d("RepoData for %d is %s", i, repoData.toJSON()); setRepoData(repoData, "pref_custom_repo_" + i); if (initial) { Preference preference = findPreference("pref_custom_repo_" + i + "_delete"); if (preference == null) continue; final int index = i; preference.setOnPreferenceClickListener(preference1 -> { if (realm.isInTransaction()) { realm.commitTransaction(); } realm.beginTransaction(); Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findFirst()).deleteFromRealm(); realm.commitTransaction(); customRepoManager.removeRepo(index); updateCustomRepoList(false); preference1.setVisible(false); return true; }); } } // any custom repo prefs larger than the number of custom repos need to be hidden. max is 5 // loop up until 5, and for each that's greater than the number of custom repos, hide it. we start at 0 // if custom repos is zero, just hide them all if (CUSTOM_REPO_ENTRIES == 0) { for (int i = 0; i < 5; i++) { Preference preference = findPreference("pref_custom_repo_" + i); if (preference == null) continue; preference.setVisible(false); } } else { for (int i = 0; i < 5; i++) { Preference preference = findPreference("pref_custom_repo_" + i); if (preference == null) continue; if (i >= CUSTOM_REPO_ENTRIES) { preference.setVisible(false); } } } Preference preference = findPreference("pref_custom_add_repo"); if (preference == null) return; preference.setVisible(customRepoManager.canAddRepo() && customRepoManager.getRepoCount() < 5); if (initial) { // Custom repo add button part. preference = findPreference("pref_custom_add_repo_button"); if (preference == null) return; preference.setOnPreferenceClickListener(preference1 -> { final Context context = this.requireContext(); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); final EditText input = new EditText(context); input.setHint(R.string.custom_url); input.setHorizontallyScrolling(true); input.setMaxLines(1); builder.setIcon(R.drawable.ic_baseline_add_box_24); builder.setTitle(R.string.add_repo); // make link in message clickable builder.setMessage(R.string.add_repo_message); builder.setView(input); builder.setPositiveButton("OK", (dialog, which) -> { String text = String.valueOf(input.getText()); text = text.trim(); // string should not be empty, start with https://, and not contain any spaces. http links are not allowed. if (text.matches("^https://.*") && !text.contains(" ") && !text.isEmpty()) { if (customRepoManager.canAddRepo(text)) { final CustomRepoData customRepoData = customRepoManager.addRepo(text); new Thread("Add Custom Repo Thread") { @Override public void run() { try { customRepoData.quickPrePopulate(); UiThreadHandler.handler.post(() -> updateCustomRepoList(false)); } catch (Exception e) { Timber.e(e); // show new dialog new Handler(Looper.getMainLooper()).post(() -> new MaterialAlertDialogBuilder(context).setTitle(R.string.error_adding).setMessage(e.getMessage()).setPositiveButton(android.R.string.ok, (dialog1, which1) -> { }).show()); } } }.start(); } else { Snackbar.make(requireView(), R.string.invalid_repo_url, BaseTransientBottomBar.LENGTH_LONG).show(); } } else { Snackbar.make(requireView(), R.string.invalid_repo_url, BaseTransientBottomBar.LENGTH_LONG).show(); } }); builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); builder.setNeutralButton("Docs", (dialog, which) -> { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Androidacy/MagiskModuleManager/blob/master/docs/DEVELOPERS.md#custom-repo-format")); startActivity(intent); }); AlertDialog alertDialog = builder.show(); final Button positiveButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); // validate as they type input.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence charSequence, int start, int before, int count) { Timber.i("checking repo url validity"); // show error if string is empty, does not start with https://, or contains spaces if (charSequence.toString().isEmpty()) { input.setError(getString(R.string.empty_field)); Timber.d("No input for repo"); positiveButton.setEnabled(false); } else if (!charSequence.toString().matches("^https://.*")) { input.setError(getString(R.string.invalid_repo_url)); Timber.d("Non https link for repo"); positiveButton.setEnabled(false); } else if (charSequence.toString().contains(" ")) { input.setError(getString(R.string.invalid_repo_url)); Timber.d("Repo url has space"); positiveButton.setEnabled(false); } else if (!customRepoManager.canAddRepo(charSequence.toString())) { input.setError(getString(R.string.repo_already_added)); Timber.d("Could not add repo for misc reason"); positiveButton.setEnabled(false); } else { // enable ok button Timber.d("Repo URL is ok"); positiveButton.setEnabled(true); } } @Override public void afterTextChanged(Editable s) { } }); positiveButton.setEnabled(false); int dp10 = FoxDisplay.dpToPixel(10), dp20 = FoxDisplay.dpToPixel(20); FoxViewCompat.setMargin(input, dp20, dp10, dp20, dp10); return true; }); } } private void setRepoData(String url) { final RepoData repoData = RepoManager.getINSTANCE().get(url); setRepoData(repoData, "pref_" + (repoData == null ? RepoManager.internalIdOfUrl(url) : repoData.preferenceId)); } private void setRepoData(final RepoData repoData, String preferenceName) { if (repoData == null) return; Timber.d("Setting preference " + preferenceName + " to " + repoData); ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE); Preference preference = findPreference(preferenceName); if (preference == null) return; if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) { if (repoData != null) { RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); RealmResults repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findAll(); Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); if (repoData.isForceHide() || repoDataRealmResults.isEmpty()) { Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden"); hideRepoData(preferenceName); return; } else { Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), repoDataRealmResults.toString()); preference.setTitle(repoData.getName()); preference.setVisible(true); // set website, support, and submitmodule as well as donate if (repoData.getWebsite() != null) { findPreference(preferenceName + "_website").setOnPreferenceClickListener((preference1 -> { IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getWebsite()); return true; })); } else { findPreference(preferenceName + "_website").setVisible(false); } if (repoData.getSupport() != null) { findPreference(preferenceName + "_support").setOnPreferenceClickListener((preference1 -> { IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSupport()); return true; })); } else { findPreference(preferenceName + "_support").setVisible(false); } if (repoData.getSubmitModule() != null) { findPreference(preferenceName + "_submit").setOnPreferenceClickListener((preference1 -> { IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSubmitModule()); return true; })); } else { findPreference(preferenceName + "_submit").setVisible(false); } if (repoData.getDonate() != null) { findPreference(preferenceName + "_donate").setOnPreferenceClickListener((preference1 -> { IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getDonate()); return true; })); } else { findPreference(preferenceName + "_donate").setVisible(false); } } realm.close(); } else { Timber.d("Hiding preference " + preferenceName + " because it's data is null"); hideRepoData(preferenceName); return; } } preference = findPreference(preferenceName + "_enabled"); if (preference != null) { // Handle custom repo separately if (repoData instanceof CustomRepoData) { preference.setTitle(R.string.custom_repo_always_on); // Disable the preference preference.setEnabled(false); return; } else { ((TwoStatePreference) preference).setChecked(repoData.isEnabled()); preference.setTitle(repoData.isEnabled() ? R.string.repo_enabled : R.string.repo_disabled); preference.setOnPreferenceChangeListener((p, newValue) -> { p.setTitle(((Boolean) newValue) ? R.string.repo_enabled : R.string.repo_disabled); // Show snackbar telling the user to refresh the modules list or restart the app Snackbar.make(requireView(), R.string.repo_enabled_changed, BaseTransientBottomBar.LENGTH_LONG).show(); return true; }); } } preference = findPreference(preferenceName + "_website"); String homepage = repoData.getWebsite(); if (preference != null) { if (!homepage.isEmpty()) { preference.setVisible(true); preference.setOnPreferenceClickListener(p -> { IntentHelper.Companion.openUrl(getFoxActivity(this), homepage); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, homepage)); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { preference.setVisible(false); } } preference = findPreference(preferenceName + "_support"); String supportUrl = repoData.getSupport(); if (preference != null) { if (supportUrl != null && !supportUrl.isEmpty()) { preference.setVisible(true); preference.setIcon(ActionButtonType.supportIconForUrl(supportUrl)); preference.setOnPreferenceClickListener(p -> { IntentHelper.Companion.openUrl(getFoxActivity(this), supportUrl); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, supportUrl)); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { preference.setVisible(false); } } preference = findPreference(preferenceName + "_donate"); String donateUrl = repoData.getDonate(); if (preference != null) { if (donateUrl != null) { preference.setVisible(true); preference.setIcon(ActionButtonType.donateIconForUrl(donateUrl)); preference.setOnPreferenceClickListener(p -> { IntentHelper.Companion.openUrl(getFoxActivity(this), donateUrl); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, donateUrl)); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { preference.setVisible(false); } } preference = findPreference(preferenceName + "_submit"); String submissionUrl = repoData.getSubmitModule(); if (preference != null) { if (submissionUrl != null && !submissionUrl.isEmpty()) { preference.setVisible(true); preference.setOnPreferenceClickListener(p -> { IntentHelper.Companion.openUrl(getFoxActivity(this), submissionUrl); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { String toastText = requireContext().getString(R.string.link_copied); clipboard.setPrimaryClip(ClipData.newPlainText(toastText, submissionUrl)); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); return true; }); } else { preference.setVisible(false); } } } private void hideRepoData(String preferenceName) { Preference preference = findPreference(preferenceName); if (preference == null) return; preference.setVisible(false); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { getPreferenceManager().setSharedPreferencesName("mmm"); setPreferencesFromResource(R.xml.repo_preferences, rootKey); applyMaterial3(getPreferenceScreen()); setRepoData(RepoManager.MAGISK_ALT_REPO); setRepoData(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT); updateCustomRepoList(true); onCreatePreferencesAndroidacy(); } } }