From 191bccec1b48421b64db706cda61282b9eaeb6c1 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Mon, 19 Jun 2023 16:44:22 -0400 Subject: [PATCH] few nips and tucks Signed-off-by: androidacy-user --- .../kotlin/com/fox2code/mmm/MainActivity.kt | 24 + .../fox2code/mmm/repo/CustomRepoManager.kt | 2 +- .../kotlin/com/fox2code/mmm/repo/RepoData.kt | 1 + .../mmm/settings/SettingsActivity.java | 1588 ------------- .../fox2code/mmm/settings/SettingsActivity.kt | 2107 +++++++++++++++++ app/src/main/res/values/strings.xml | 1 + gradle.properties | 8 +- 7 files changed, 2136 insertions(+), 1595 deletions(-) delete mode 100644 app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java create mode 100644 app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt diff --git a/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt index de146ca..a14bc9f 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt @@ -28,6 +28,7 @@ import androidx.appcompat.widget.SearchView import androidx.cardview.widget.CardView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.room.Room import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.fox2code.foxcompat.app.FoxActivity @@ -54,6 +55,7 @@ import com.fox2code.mmm.utils.RuntimeUtils import com.fox2code.mmm.utils.SyncManager import com.fox2code.mmm.utils.io.net.Http.Companion.cleanDnsCache import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView +import com.fox2code.mmm.utils.room.ReposListDatabase import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.progressindicator.LinearProgressIndicator import org.matomo.sdk.extra.TrackHelper @@ -108,6 +110,28 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis // Show a toast to warn the user Toast.makeText(this, R.string.not_official_build, Toast.LENGTH_LONG).show() } + // track enabled repos + Thread { + val db = Room.databaseBuilder( + applicationContext, + ReposListDatabase::class.java, + "ReposList.db" + ).build() + val repoDao = db.reposListDao() + val repos = repoDao.getAll() + val enabledRepos = StringBuilder() + for (repo in repos) { + if (repo.enabled) { + enabledRepos.append(repo.url).append(", ") + } + } + db.close() + if (enabledRepos.isNotEmpty()) { + enabledRepos.delete(enabledRepos.length - 2, enabledRepos.length) + TrackHelper.track().event("Enabled Repos", enabledRepos.toString()) + .with(MainApplication.INSTANCE!!.tracker) + } + }.start() val ts = Timestamp(System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000) // check if this build has expired val buildTime = Timestamp(BuildConfig.BUILD_TIME) diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt index cc8038a..823ff2a 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt @@ -112,7 +112,7 @@ class CustomRepoManager internal constructor( // now the same as above but for room database val applicationContext = INSTANCE!!.applicationContext val db = Room.databaseBuilder( - applicationContext, ReposListDatabase::class.java, "reposlist.db" + applicationContext, ReposListDatabase::class.java, "ReposList.db" ).build() val reposListDao = db.reposListDao() val reposList = ReposList(id, repo, true, donate, support, submitModule, 0, name, website) diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt index 255ed26..b2cbbe9 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt @@ -297,6 +297,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { "ReposList.db", ).allowMainThreadQueries().build() val reposList = db.reposListDao().getById(preferenceId!!) + db.close() // should never happen but for safety if (reposList.enabled) { !isForceHide diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java deleted file mode 100644 index 0b5fb24..0000000 --- a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java +++ /dev/null @@ -1,1588 +0,0 @@ -/* - * 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 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.room.Room; -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.room.ReposList; -import com.fox2code.mmm.utils.room.ReposListDatabase; -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.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Random; -import java.util.Set; - -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() { - // special algorithm to determine performance class. low is < 4 cores and/ore < 4GB ram, mid is 4-6 cores and 4-6GB ram, high is > 6 cores and > 6GB ram. android sdk version is used as well - // device is awarded 1 point for each core and 1 point for each GB of ram. - - int points = 0; - int cores = Runtime.getRuntime().availableProcessors(); - ActivityManager activityManager = (ActivityManager) Objects.requireNonNull(MainApplication.getINSTANCE()).getSystemService(Context.ACTIVITY_SERVICE); - if (activityManager != null) { - ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); - activityManager.getMemoryInfo(memoryInfo); - long totalMemory = memoryInfo.totalMem; - points += cores; - points += totalMemory / 1024 / 1024 / 1024; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - points += 1; - } - Timber.d("Device performance class: %d", points); - if (points <= 7) { - return PERFORMANCE_CLASS_LOW; - } else if (points <= 12) { - return PERFORMANCE_CLASS_AVERAGE; - } else { - return PERFORMANCE_CLASS_HIGH; - } - } - - @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()); - fileOutputStream.write(("Beginning of logs:\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 room reposlist db - ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); - - // add listener to magisk_alt_repo_enabled switch to update room db - Preference magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled")); - magiskAltRepoEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - // Update room db - db.reposListDao().setEnabled("magisk_alt_repo", Boolean.parseBoolean(String.valueOf(newValue))); - 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 room db - db.reposListDao().setEnabled("androidacy_repo", false); - return false; - }); - } else { - // get if androidacy repo is enabled from room db - ReposList repoRealmResults = db.reposListDao().getById("androidacy_repo"); - boolean androidacyRepoEnabledPref = repoRealmResults.getEnabled(); - // 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 - db.reposListDao().setEnabled("androidacy_repo", 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) { - // get all repos that are not built-in - int CUSTOM_REPO_ENTRIES = 0; - // array of custom repos - ArrayList customRepos = new ArrayList<>(); - ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); - List reposList = db.reposListDao().getAll(); - for (ReposList repo : reposList) { - ArrayList buildInRepos = new ArrayList<>(Arrays.asList("androidacy_repo", "magisk_alt_repo")); - if (!buildInRepos.contains(repo.getId())) { - CUSTOM_REPO_ENTRIES++; - customRepos.add(repo.getId()); - } - } - 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 -> { - db.reposListDao().delete(customRepos.get(index)); - 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) { - ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); - ReposList reposList = db.reposListDao().getById(repoData.preferenceId); - Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); - if (repoData.isForceHide() || reposList == null) { - 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(), reposList); - 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); - } - } - } 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(); - } - } -} diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt new file mode 100644 index 0000000..93635e8 --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt @@ -0,0 +1,2107 @@ +/* + * 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 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.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.Toast +import androidx.annotation.StringRes +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.Preference.SummaryProvider +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroup +import androidx.preference.SwitchPreferenceCompat +import androidx.preference.TwoStatePreference +import androidx.room.Room.databaseBuilder +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.Companion.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.MainApplication.Companion.INSTANCE +import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences +import com.fox2code.mmm.MainApplication.Companion.isBackgroundUpdateCheckEnabled +import com.fox2code.mmm.MainApplication.Companion.isCrashReportingEnabled +import com.fox2code.mmm.MainApplication.Companion.isDeveloper +import com.fox2code.mmm.MainApplication.Companion.isFirstBoot +import com.fox2code.mmm.MainApplication.Companion.isNotificationPermissionGranted +import com.fox2code.mmm.R +import com.fox2code.mmm.UpdateActivity +import com.fox2code.mmm.androidacy.AndroidacyRepoData +import com.fox2code.mmm.background.BackgroundUpdateChecker.Companion.onMainActivityResume +import com.fox2code.mmm.background.BackgroundUpdateChecker.Companion.postNotification +import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath +import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion +import com.fox2code.mmm.manager.LocalModuleInfo +import com.fox2code.mmm.manager.ModuleManager +import com.fox2code.mmm.module.ActionButtonType.Companion.donateIconForUrl +import com.fox2code.mmm.module.ActionButtonType.Companion.supportIconForUrl +import com.fox2code.mmm.repo.CustomRepoData +import com.fox2code.mmm.repo.RepoData +import com.fox2code.mmm.repo.RepoManager +import com.fox2code.mmm.repo.RepoManager.Companion.getINSTANCE +import com.fox2code.mmm.repo.RepoManager.Companion.internalIdOfUrl +import com.fox2code.mmm.settings.LongClickablePreference.OnPreferenceLongClickListener +import com.fox2code.mmm.utils.ExternalHelper +import com.fox2code.mmm.utils.IntentHelper.Companion.openUrl +import com.fox2code.mmm.utils.ProcessHelper.Companion.restartApplicationProcess +import com.fox2code.mmm.utils.io.net.Http.Companion.setDoh +import com.fox2code.mmm.utils.room.ReposListDatabase +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 timber.log.Timber +import java.io.BufferedReader +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.util.Objects +import java.util.Random +import kotlin.system.exitProcess + +@Suppress("SENSELESS_COMPARISON") +class SettingsActivity : FoxActivity(), LanguageActivity { + @SuppressLint("RestrictedApi") + private val onItemSelectedListener = + NavigationBarView.OnItemSelectedListener { item: MenuItem -> + val itemId = item.itemId + if (itemId == R.id.back) { + TrackHelper.track().event("view_list", "main_modules") + .with(INSTANCE!!.getTracker()) + startActivity(Intent(this, MainActivity::class.java)) + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + finish() + return@OnItemSelectedListener true + } else if (itemId == R.id.settings_menu_item) { + return@OnItemSelectedListener true + } + false + } + + @SuppressLint("RestrictedApi") + override fun onCreate(savedInstanceState: Bundle?) { + devModeStep = 0 + super.onCreate(savedInstanceState) + TrackHelper.track().screen(this).with(INSTANCE!!.getTracker()) + setContentView(R.layout.settings_activity) + setTitle(R.string.app_name) + //hideActionBar(); + val bottomNavigationView = findViewById(R.id.bottom_navigation) + bottomNavigationView.setOnItemSelectedListener(onItemSelectedListener) + if (savedInstanceState == null) { + val settingsFragment = SettingsFragment() + supportFragmentManager.beginTransaction().replace(R.id.settings, settingsFragment) + .commit() + } + } + + @SuppressLint("InlinedApi") + override fun refreshRosettaX() { + restartApplicationProcess(this) + } + + override fun onPause() { + onMainActivityResume(this) + super.onPause() + } + + annotation class PerformanceClass + class SettingsFragment : PreferenceFragmentCompat(), OnBackPressedCallback { + @SuppressLint("UnspecifiedImmutableFlag") + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val name = "mmmx" + val context: Context? = INSTANCE + val masterKey: MasterKey + val preferenceManager = preferenceManager + val dataStore: SharedPreferenceDataStore + val editor: SharedPreferences.Editor + try { + masterKey = + MasterKey.Builder(context!!).setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + dataStore = SharedPreferenceDataStore( + EncryptedSharedPreferences.create( + context, + name, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + ) + preferenceManager!!.preferenceDataStore = dataStore + preferenceManager.sharedPreferencesName = "mmm" + editor = dataStore.sharedPreferences.edit() + } catch (e: Exception) { + Timber.e(e, "Failed to create encrypted shared preferences") + throw RuntimeException(getString(R.string.error_encrypted_shared_preferences)) + } + setPreferencesFromResource(R.xml.root_preferences, rootKey) + RepoFragment.applyMaterial3(preferenceScreen) + // track all non empty values + val sharedPreferences = dataStore.sharedPreferences + // disabled until EncryptedSharedPreferences fixes getAll() + // add bottom navigation bar to the settings + val bottomNavigationView = + requireActivity().findViewById(R.id.bottom_navigation) + if (bottomNavigationView != null) { + bottomNavigationView.visibility = View.VISIBLE + bottomNavigationView.menu.findItem(R.id.settings_menu_item).isChecked = true + } + findPreference("pref_manage_repos")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + devModeStep = 0 + openFragment(RepoFragment(), R.string.manage_repos_pref) + true + } + val themePreference = findPreference("pref_theme") + // If transparent theme(s) are set, disable monet + if (themePreference!!.value == "transparent_light") { + Timber.d("disabling monet") + findPreference("pref_enable_monet")!!.isEnabled = false + // Toggle monet off + (findPreference("pref_enable_monet") as TwoStatePreference?)!!.isChecked = + 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")!!.isEnabled = false + (findPreference("pref_enable_blur") as TwoStatePreference?)!!.isChecked = + false + editor.putBoolean("pref_enable_blur", false).apply() + findPreference("pref_enable_blur")!!.setSummary(R.string.blur_disabled_summary) + } + themePreference.summaryProvider = + SummaryProvider { _: Preference? -> themePreference.entry } + themePreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + // You need to reboot your device at least once to be able to access dev-mode + if (devModeStepFirstBootIgnore || !isFirstBoot) devModeStep = 1 + Timber.d("refreshing activity. New value: %s", newValue) + editor.putString("pref_theme", newValue as String).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 + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.transparent_theme_dialogue_title) + .setMessage( + R.string.transparent_theme_dialogue_message + ) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + // Toggle monet off + (findPreference("pref_enable_monet") as TwoStatePreference?)!!.isChecked = + 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") as TwoStatePreference?)!!.isChecked = + 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({ + INSTANCE!!.updateTheme() + getFoxActivity(this).setThemeRecreate(INSTANCE!!.getManagerThemeResId()) + }, 1) + val intent = Intent(requireContext(), SettingsActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + .setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> + // Revert to system theme + (findPreference("pref_theme") as ListPreference?)!!.value = + "system" + // Refresh activity + devModeStep = 0 + }.show() + } else { + findPreference("pref_enable_monet")!!.isEnabled = true + findPreference("pref_enable_monet")?.summary = "" + findPreference("pref_enable_blur")!!.isEnabled = true + findPreference("pref_enable_blur")?.summary = "" + devModeStep = 0 + } + UiThreadHandler.handler.postDelayed({ + INSTANCE!!.updateTheme() + getFoxActivity(this).setThemeRecreate(INSTANCE!!.getManagerThemeResId()) + }, 1) + true + } + // Crash reporting + val crashReportingPreference = + findPreference("pref_crash_reporting") + if (!SentryMain.IS_SENTRY_INSTALLED) crashReportingPreference!!.isVisible = false + crashReportingPreference!!.isChecked = isCrashReportingEnabled + val initialValue: Any = isCrashReportingEnabled + crashReportingPreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener setOnPreferenceChangeListener@{ _: Preference?, newValue: Any -> + devModeStepFirstBootIgnore = true + devModeStep = 0 + if (initialValue === newValue) return@setOnPreferenceChangeListener true + // Show a dialog to restart the app + val materialAlertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) + materialAlertDialogBuilder.setTitle(R.string.crash_reporting_restart_title) + materialAlertDialogBuilder.setMessage(R.string.crash_reporting_restart_message) + materialAlertDialogBuilder.setPositiveButton(R.string.restart) { _: DialogInterface?, _: Int -> + val mStartActivity = Intent(requireContext(), MainActivity::class.java) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + // If < 23, FLAG_IMMUTABLE is not available + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = requireContext().getSystemService(ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = mPendingIntent + Timber.d("Restarting app to save crash reporting preference: %s", newValue) + exitProcess(0) // Exit app process + } + // Do not reverse the change if the user cancels the dialog + materialAlertDialogBuilder.setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> } + materialAlertDialogBuilder.show() + true + } + val enableBlur = findPreference("pref_enable_blur") + // Disable blur on low performance devices + if (devicePerformanceClass < PERFORMANCE_CLASS_AVERAGE) { + // Show a warning + enableBlur!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + if (newValue == true) { + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.low_performance_device_dialogue_title) + .setMessage( + R.string.low_performance_device_dialogue_message + ) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + // Toggle blur on + (findPreference("pref_enable_blur") as TwoStatePreference?)!!.isChecked = + true + editor.putBoolean("pref_enable_blur", true).apply() + // Set summary + findPreference("pref_enable_blur")!!.setSummary(R.string.blur_disabled_summary) + } + .setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> + // Revert to blur on + (findPreference("pref_enable_blur") as TwoStatePreference?)!!.isChecked = + false + editor.putBoolean("pref_enable_blur", false).apply() + // Set summary + findPreference("pref_enable_blur")?.summary = + getString(R.string.blur_performance_warning_summary) + }.show() + } + true + } + } + val disableMonet = findPreference("pref_enable_monet") + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + disableMonet!!.setSummary(R.string.require_android_12) + disableMonet.isEnabled = false + } + disableMonet!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + UiThreadHandler.handler.postDelayed({ + INSTANCE!!.updateTheme() + (requireActivity() as FoxActivity).setThemeRecreate(INSTANCE!!.getManagerThemeResId()) + }, 1) + true + } + findPreference("pref_dns_over_https")!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, v: Any? -> + setDoh( + (v as Boolean?)!! + ) + true + } + + // handle restart required for showcase mode + findPreference("pref_showcase_mode")!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, v: Any -> + if (v == true) { + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.restart) + .setMessage(R.string.showcase_mode_dialogue_message).setPositiveButton( + R.string.ok + ) { _: DialogInterface?, _: Int -> + // Toggle showcase mode on + (findPreference("pref_showcase_mode") as TwoStatePreference?)!!.isChecked = + true + editor.putBoolean("pref_showcase_mode", true).apply() + // restart app + val mStartActivity = Intent(requireContext(), MainActivity::class.java) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = + requireContext().getSystemService(ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = mPendingIntent + Timber.d("Restarting app to save showcase mode preference: %s", v) + exitProcess(0) // Exit app process + } + .setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> + // Revert to showcase mode on + (findPreference("pref_showcase_mode") as TwoStatePreference?)!!.isChecked = + false + editor.putBoolean("pref_showcase_mode", false).apply() + // restart app + val mStartActivity = + Intent(requireContext(), MainActivity::class.java) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = + requireContext().getSystemService(ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = + mPendingIntent + Timber.d("Restarting app to save showcase mode preference: %s", v) + exitProcess(0) // Exit app process + }.show() + } + true + } + val languageSelector = findPreference("pref_language_selector") + languageSelector!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + val ls = LanguageSwitcher( + requireActivity() + ) + ls.setSupportedStringLocales(MainApplication.supportedLocales) + ls.showChangeLanguageDialog(activity) + true + } + + // Handle pref_language_selector_cta by taking user to https://translate.nift4.org/engage/foxmmm/ + val languageSelectorCta = + findPreference("pref_language_selector_cta") + languageSelectorCta!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://translate.nift4.org/engage/foxmmm/") + ) + startActivity(browserIntent) + true + } + + // Long click to copy url + languageSelectorCta.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val clipboard = + requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clip = + ClipData.newPlainText("URL", "https://translate.nift4.org/engage/foxmmm/") + clipboard.setPrimaryClip(clip) + Toast.makeText(requireContext(), R.string.link_copied, Toast.LENGTH_SHORT) + .show() + true + } + val level = currentLanguageLevel() + if (level != LANGUAGE_SUPPORT_LEVEL) { + Timber.e("latest is %s", LANGUAGE_SUPPORT_LEVEL) + languageSelector.setSummary(R.string.language_support_outdated) + } else { + val translatedBy = this.getString(R.string.language_translated_by) + // I don't "translate" english + if (!("Translated by Fox2Code (Put your name here)" == translatedBy || "Translated by Fox2Code" == translatedBy)) { + languageSelector.setSummary(R.string.language_translated_by) + } else { + languageSelector.summary = null + } + } + if (!isDeveloper) { + findPreference("pref_disable_low_quality_module_filter")!!.isVisible = + false + // Find pref_clear_data and set it invisible + findPreference("pref_clear_data")!!.isVisible = false + } + // hande clear cache + findPreference("pref_clear_cache")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // Clear cache + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.clear_cache_dialogue_title) + .setMessage( + R.string.clear_cache_dialogue_message + ).setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + // Clear app cache + try { + // use apache commons IO to delete the cache + FileUtils.deleteDirectory(requireContext().cacheDir) + // create a new cache dir + FileUtils.forceMkdir(requireContext().cacheDir) + // create cache dirs for cronet and webview + FileUtils.forceMkdir(File(requireContext().cacheDir, "cronet")) + FileUtils.forceMkdir(File(INSTANCE!!.dataDir.toString() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm")) + FileUtils.forceMkdir(File(INSTANCE!!.dataDir.toString() + "/cache/WebView/Default/HTTP Cache/Code Cache/js")) + Toast.makeText( + requireContext(), + R.string.cache_cleared, + Toast.LENGTH_SHORT + ).show() + } catch (e: Exception) { + Timber.e(e) + Toast.makeText( + requireContext(), + R.string.cache_clear_failed, + Toast.LENGTH_SHORT + ).show() + } + }.setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> } + .show() + true + } + if (!SentryMain.IS_SENTRY_INSTALLED || !BuildConfig.DEBUG || peekMagiskPath() == null) { + // Hide the pref_crash option if not in debug mode - stop users from purposely crashing the app + Timber.i(peekMagiskPath()) + findPreference("pref_test_crash")!!.isVisible = false + } else { + if (findPreference("pref_test_crash") != null && findPreference( + "pref_clear_data" + ) != null + ) { + findPreference("pref_test_crash")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + throw 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")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // Clear app data + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.clear_data_dialogue_title) + .setMessage( + R.string.clear_data_dialogue_message + ) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + // Clear app data + INSTANCE!!.resetApp() + } + .setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> } + .show() + true + } + } else { + Timber.e( + "Something is null: %s, %s", + findPreference("pref_clear_data"), + findPreference("pref_test_crash") + ) + } + } + if (peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND || !isDeveloper) { + findPreference("pref_use_magisk_install_command")!!.isVisible = false + } + val debugNotification = findPreference("pref_background_update_check_debug") + val updateCheckExcludes = + findPreference("pref_background_update_check_excludes") + val updateCheckVersionExcludes = + findPreference("pref_background_update_check_excludes_version") + debugNotification!!.isEnabled = isBackgroundUpdateCheckEnabled + debugNotification.isVisible = + isDeveloper && !MainApplication.isWrapped && isBackgroundUpdateCheckEnabled + debugNotification.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // fake updatable modules hashmap + val updateableModules = HashMap() + // count of modules to fake must match the count in the random number generator + val random = Random() + var count: Int + do { + count = random.nextInt(4) + 2 + } while (count == 2) + for (i in 0 until count) { + var fakeVersion: Int + do { + fakeVersion = random.nextInt(10) + } while (fakeVersion == 0) + Timber.d("Fake version: %s, count: %s", fakeVersion, i) + updateableModules["FakeModule $i"] = "1.0.$fakeVersion" + } + postNotification(requireContext(), updateableModules, count, true) + true + } + val backgroundUpdateCheck = findPreference("pref_background_update_check") + backgroundUpdateCheck!!.isVisible = !MainApplication.isWrapped + // Make uncheckable if POST_NOTIFICATIONS permission is not granted + if (!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.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // set the box to unchecked + (backgroundUpdateCheck as SwitchPreferenceCompat?)!!.isChecked = false + // ensure that the preference is false + getSharedPreferences("mmm")!! + .edit().putBoolean("pref_background_update_check", false).apply() + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.permission_notification_title) + .setMessage( + R.string.permission_notification_message + ) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + // Open the app settings + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri = + Uri.fromParts("package", requireContext().packageName, null) + intent.data = uri + this.startActivity(intent) + } + .setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> } + .show() + true + } + backgroundUpdateCheck.setSummary(R.string.background_update_check_permission_required) + } + updateCheckExcludes!!.isVisible = + isBackgroundUpdateCheckEnabled && !MainApplication.isWrapped + backgroundUpdateCheck.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + val enabled = java.lang.Boolean.parseBoolean(newValue.toString()) + debugNotification.isEnabled = enabled + debugNotification.isVisible = + isDeveloper && !MainApplication.isWrapped && enabled + updateCheckExcludes.isEnabled = enabled + updateCheckExcludes.isVisible = enabled && !MainApplication.isWrapped + if (!enabled) { + onMainActivityResume(requireContext()) + } + 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.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + val localModuleInfos: Collection = + ModuleManager.instance!!.modules.values + // make sure we have modules + val checkedItems: BooleanArray + if (!localModuleInfos.isEmpty()) { + val moduleNames = arrayOfNulls(localModuleInfos.size) + checkedItems = BooleanArray(localModuleInfos.size) + // get the stringset pref_background_update_check_excludes + val stringSetTemp = sharedPreferences.getStringSet( + "pref_background_update_check_excludes", + HashSet() + ) + // copy to a new set so we can modify it + val stringSet: MutableSet = HashSet(stringSetTemp!!) + for ((i, localModuleInfo) in localModuleInfos.withIndex()) { + 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]) + } + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.background_update_check_excludes) + .setMultiChoiceItems( + moduleNames, + checkedItems + ) { _: DialogInterface?, which: Int, isChecked: Boolean -> + // get id from name + val id: String = if (localModuleInfos.stream() + .anyMatch { localModuleInfo: LocalModuleInfo? -> localModuleInfo!!.name == moduleNames[which] } + ) { + localModuleInfos.stream() + .filter { localModuleInfo: LocalModuleInfo? -> + localModuleInfo!!.name.equals( + moduleNames[which] + ) + }.findFirst().orElse(null)!!.id + } else { + "" + } + if (id.isNotEmpty()) { + if (isChecked) { + stringSet.add(id) + } else { + stringSet.remove(id) + } + } + sharedPreferences.edit().putStringSet( + "pref_background_update_check_excludes", + stringSet + ).apply() + } + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> } + .show() + } else { + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.background_update_check_excludes) + .setMessage( + R.string.background_update_check_excludes_no_modules + ) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> } + .show() + } + true + } + // now handle pref_background_update_check_excludes_version + updateCheckVersionExcludes!!.isVisible = + isBackgroundUpdateCheckEnabled && !MainApplication.isWrapped + updateCheckVersionExcludes.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + // get the stringset pref_background_update_check_excludes_version + val stringSet = sharedPreferences.getStringSet( + "pref_background_update_check_excludes_version", + 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 + val localModuleInfos: Collection = + ModuleManager.instance!!.modules.values + // make sure we have modules + if (localModuleInfos.isEmpty()) { + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.background_update_check_excludes) + .setMessage( + R.string.background_update_check_excludes_no_modules + ) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> } + .show() + } else { + val layout = LinearLayout(requireContext()) + layout.orientation = LinearLayout.VERTICAL + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.setMargins(48, 0, 48, 0) + // add a summary + val textView = MaterialTextView(requireContext()) + textView.layoutParams = params + textView.setText(R.string.background_update_check_excludes_version_summary) + for (localModuleInfo in localModuleInfos) { + // two views: materialtextview for name, edittext for version + val materialTextView = MaterialTextView(requireContext()) + materialTextView.layoutParams = params + materialTextView.setPadding(12, 8, 12, 8) + materialTextView.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Subtitle1) + materialTextView.text = localModuleInfo!!.name + layout.addView(materialTextView) + val editText = EditText(requireContext()) + editText.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + editText.layoutParams = 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 + val id = localModuleInfos.stream() + .filter { localModuleInfo1: LocalModuleInfo? -> + localModuleInfo1!!.name.equals( + localModuleInfo.name + ) + }.findFirst().orElse(null)!!.id + val version = + stringSet!!.stream().filter { s: String -> s.startsWith(id) } + .findFirst().orElse("") + if (version.isNotEmpty()) { + editText.setText( + version.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[1] + ) + } + layout.addView(editText) + } + val scrollView = ScrollView(requireContext()) + scrollView.addView(layout) + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.background_update_check_excludes_version) + .setView(scrollView).setPositiveButton( + R.string.ok + ) { _: DialogInterface?, _: Int -> + Timber.d("ok clicked") + // for every module, get the text field and save it to the stringset + val stringSetTemp: MutableSet = HashSet() + var prevMod = "" + for (i in 0 until layout.childCount) { + if (layout.getChildAt(i) is MaterialTextView) { + val mv = layout.getChildAt(i) as MaterialTextView + prevMod = mv.text.toString() + continue + } + val editText = layout.getChildAt(i) as EditText + var text = editText.text.toString() + if (text.isNotEmpty()) { + // text can only contain numbers and the characters ^ and $ + // so we remove all non-numbers and non ^ and $ + text = text.replace("[^0-9^$]".toRegex(), "") + // we have to use module id even though we show name + val finalprevMod = prevMod + stringSetTemp.add( + localModuleInfos.stream() + .filter { localModuleInfo: LocalModuleInfo? -> + localModuleInfo!!.name.equals(finalprevMod) + } + .findFirst().orElse(null)!!.id + ":" + text) + Timber.d("text is %s for %s", text, editText.hint.toString()) + } else { + Timber.d("text is empty for %s", editText.hint.toString()) + } + } + sharedPreferences.edit().putStringSet( + "pref_background_update_check_excludes_version", + stringSetTemp + ).apply() + } + .setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> } + .show() + } + true + } + val libsBuilder = LibsBuilder().withShowLoadingProgress(false).withLicenseShown(true) + .withAboutMinimalDesign(false) + val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + var linkClickable = findPreference("pref_update") + linkClickable!!.isVisible = + BuildConfig.ENABLE_AUTO_UPDATER && (BuildConfig.DEBUG || appUpdateManager.peekHasUpdate()) + linkClickable.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + devModeStep = 0 + // open UpdateActivity with CHECK action + val intent = Intent(requireContext(), UpdateActivity::class.java) + intent.action = UpdateActivity.ACTIONS.CHECK.name + startActivity(intent) + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val 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() + true + } + // for pref_background_update_check_debug_download, do the same as pref_update except with DOWNLOAD action + val debugDownload = + findPreference("pref_background_update_check_debug_download") + debugDownload!!.isVisible = + isDeveloper && isBackgroundUpdateCheckEnabled && !MainApplication.isWrapped + debugDownload.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + devModeStep = 0 + val intent = Intent(requireContext(), UpdateActivity::class.java) + intent.action = UpdateActivity.ACTIONS.DOWNLOAD.name + startActivity(intent) + true + } + if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) { + linkClickable = findPreference("pref_report_bug") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + devModeStep = 0 + devModeStepFirstBootIgnore = true + openUrl( + p.context, + "https://github.com/Androidacy/MagiskModuleManager/issues" + ) + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val 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() + true + } + } else { + findPreference("pref_report_bug")!!.isVisible = 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 + var userRepo = BuildConfig.REMOTE_URL + // remove .git + userRepo = userRepo.replace("\\.git$".toRegex(), "") + 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 + val finalUserRepo = userRepo.replace( + "^(https?://)?(www\\.)?(github\\.com|gitlab\\.com|bitbucket\\.org|git\\.io|git\\.me|git\\.net|git\\.xyz|git\\.tk|git\\.co)/".toRegex(), + "" + ) + linkClickable!!.summary = String.format( + getString(R.string.source_code_summary), + BuildConfig.COMMIT_HASH, + finalUserRepo + ) + Timber.d("finalUserRepo: %s", finalUserRepo) + val finalUserRepo1 = userRepo + linkClickable.onPreferenceClickListener = + Preference.OnPreferenceClickListener setOnPreferenceClickListener@{ p: Preference -> + if (devModeStep == 2) { + devModeStep = 0 + if (isDeveloper && !BuildConfig.DEBUG) { + 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 { + 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(requireContext()) + return@setOnPreferenceClickListener true + } + // build url from BuildConfig.REMOTE_URL and BuildConfig.COMMIT_HASH. May have to remove the .git at the end + openUrl( + p.context, + finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH + ) + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val 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() + true + } + // Next, the pref_androidacy_thanks should lead to the androidacy website + linkClickable = findPreference("pref_androidacy_thanks") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + openUrl( + p.context, + "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager" + ) + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val 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() + true + } + // pref_fox2code_thanks should lead to https://github.com/Fox2Code + linkClickable = findPreference("pref_fox2code_thanks") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + openUrl(p.context, "https://github.com/Fox2Code") + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip( + ClipData.newPlainText( + toastText, + "https://github.com/Fox2Code" + ) + ) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + // handle pref_save_logs which saves logs to our external storage and shares them + val saveLogs = findPreference("pref_save_logs") + saveLogs!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener setOnPreferenceClickListener@{ _: Preference? -> + // Save logs to external storage + val logsFile = File(requireContext().getExternalFilesDir(null), "logs.txt") + var fileOutputStream: FileOutputStream? = null + try { + logsFile.createNewFile() + fileOutputStream = FileOutputStream(logsFile) + // first, write some info about the device + fileOutputStream.write( + """FoxMagiskModuleManager version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) + """.toByteArray() + ) + fileOutputStream.write( + """Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) + """.toByteArray() + ) + fileOutputStream.write( + """Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE}) + """.toByteArray() + ) + fileOutputStream.write( + """Magisk version: ${peekMagiskVersion()} + """.toByteArray() + ) + fileOutputStream.write( + ("Has internet: " + (if (getINSTANCE()!! + .hasConnectivity() + ) "Yes" else "No") + "\n").toByteArray() + ) + fileOutputStream.write("Beginning of logs:\n".toByteArray()) + + // read our logcat but format the output to be more readable + val process = Runtime.getRuntime().exec("logcat -d -v tag") + val bufferedReader = BufferedReader(InputStreamReader(process.inputStream)) + var line: String + while (bufferedReader.readLine().also { line = it } != null) { + fileOutputStream.write( + """$line + """.toByteArray() + ) + } + } catch (e: IOException) { + e.printStackTrace() + Toast.makeText( + requireContext(), + R.string.error_saving_logs, + Toast.LENGTH_SHORT + ).show() + return@setOnPreferenceClickListener true + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close() + } catch (ignored: IOException) { + } + } + } + // Share logs + val shareIntent = Intent() + // create a new intent and grantUriPermission to the file provider + shareIntent.action = 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.type = "text/plain" + startActivity(Intent.createChooser(shareIntent, getString(R.string.share_logs))) + true + } + // pref_contributors should lead to the contributors page + linkClickable = findPreference("pref_contributors") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + // Remove the .git if it exists and add /graphs/contributors + var url = BuildConfig.REMOTE_URL + if (url.endsWith(".git")) { + url = url.substring(0, url.length - 4) + } + url += "/graphs/contributors" + openUrl(p.context, url) + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val toastText = requireContext().getString(R.string.link_copied) + // Remove the .git if it exists and add /graphs/contributors + var 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() + true + } + linkClickable = findPreference("pref_support") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + devModeStep = 0 + openUrl(p.context, "https://t.me/Fox2Code_Chat") + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val 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() + true + } + // pref_announcements to https://t.me/androidacy + linkClickable = findPreference("pref_announcements") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + devModeStep = 0 + openUrl(p.context, "https://t.me/androidacy") + true + } + linkClickable.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip( + ClipData.newPlainText( + toastText, + "https://t.me/androidacy" + ) + ) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + findPreference("pref_show_licenses")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + devModeStep = if (devModeStep == 1) 2 else 0 + onMainActivityResume(requireContext()) + openFragment(libsBuilder.supportFragment(), R.string.licenses) + true + } + // Determine if this is an official build based on the signature + val flavor = BuildConfig.FLAVOR + val type = BuildConfig.BUILD_TYPE + // Set the summary of pref_pkg_info to something like default-debug v1.0 (123) (Official) + val pkgInfo = getString( + R.string.pref_pkg_info_summary, + "$flavor-$type", + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE, + if (MainApplication.o) getString( + R.string.official + ) else getString(R.string.unofficial) + ) + findPreference("pref_pkg_info")!!.summary = pkgInfo + // special easter egg :) + var versionClicks = 0 + findPreference("pref_pkg_info")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + versionClicks++ + Timber.d("Version clicks: %d", versionClicks) + // if it's been 3 clicks, toast "yer a wizard, harry" or "keep tapping to enter hogwarts" + if (versionClicks == 3) { + // random choice of 1 or 2 + val rand = Random() + val n = rand.nextInt(2) + 1 + Toast.makeText( + p.context, + if (n == 1) R.string.yer_a_wizard_harry else R.string.keep_tapping_to_enter_hogwarts, + Toast.LENGTH_SHORT + ).show() + } + if (versionClicks == 7) { + versionClicks = 0 + openUrl(p.context, "https://www.youtube.com/watch?v=dQw4w9WgXcQ") + } + true + } + val prefDonateFox = findPreference("pref_donate_fox") + if (BuildConfig.FLAVOR != "play") { + prefDonateFox!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // open fox + openUrl(getFoxActivity(this), "https://paypal.me/fox2code") + true + } + // handle long click on pref_donate_fox + prefDonateFox.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + // copy to clipboard + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip( + ClipData.newPlainText( + toastText, + "https://paypal.me/fox2code" + ) + ) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + } else { + prefDonateFox!!.isVisible = false + } + // now handle pref_donate_androidacy + val prefDonateAndroidacy = + findPreference("pref_donate_androidacy") + if (BuildConfig.FLAVOR != "play") { + if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == "Guest" || AndroidacyRepoData.instance.memberLevel == null) { + prefDonateAndroidacy!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // copy FOX2CODE promo code to clipboard and toast user that they can use it for half off any subscription + val toastText = requireContext().getString(R.string.promo_code_copied) + clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "FOX2CODE")) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + // open androidacy + openUrl( + getFoxActivity(this), + "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate" + ) + true + } + // handle long click on pref_donate_androidacy + prefDonateAndroidacy.onPreferenceLongClickListener = + OnPreferenceLongClickListener { _: Preference? -> + // copy to clipboard + val 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() + true + } + } else { + // set text to "Thank you for your support!" + prefDonateAndroidacy!!.setSummary(R.string.androidacy_thanks_up) + prefDonateAndroidacy.setTitle(R.string.androidacy_thanks_up_title) + } + } else { + prefDonateAndroidacy!!.isVisible = false + } + } + + private fun openFragment(fragment: Fragment, @StringRes title: Int) { + val compatActivity = getFoxActivity(this) + compatActivity.onBackPressedCallback = this + compatActivity.setTitle(title) + compatActivity.supportFragmentManager.beginTransaction() + .replace(R.id.settings, fragment).setTransition( + FragmentTransaction.TRANSIT_FRAGMENT_FADE + ).commit() + } + + override fun onBackPressed(compatActivity: FoxActivity): Boolean { + compatActivity.setTitle(R.string.app_name) + compatActivity.supportFragmentManager.beginTransaction().replace(R.id.settings, this) + .setTransition( + FragmentTransaction.TRANSIT_FRAGMENT_FADE + ).commit() + return true + } + + private fun currentLanguageLevel(): Int { + val declaredLanguageLevel = this.resources.getInteger(R.integer.language_support_level) + if (declaredLanguageLevel != LANGUAGE_SUPPORT_LEVEL) return declaredLanguageLevel + return if (this.resources.configuration.locales[0].language != "en" && this.resources.getString( + R.string.notification_update_pref + ) == "Background modules update check" && this.resources.getString(R.string.notification_update_desc) == "May increase battery usage" + ) { + 0 + } else LANGUAGE_SUPPORT_LEVEL + } + } + + class RepoFragment : PreferenceFragmentCompat() { + @SuppressLint("RestrictedApi", "UnspecifiedImmutableFlag") + fun onCreatePreferencesAndroidacy() { + // Bind the pref_show_captcha_webview to captchaWebview('https://production-api.androidacy.com/') + // Also require dev mode + // CaptchaWebview.setVisible(false); + val androidacyTestMode = + findPreference("pref_androidacy_test_mode")!! + if (!isDeveloper) { + androidacyTestMode.isVisible = false + } else { + // Show a warning if user tries to enable test mode + androidacyTestMode.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + if (java.lang.Boolean.parseBoolean(newValue.toString())) { + // Use MaterialAlertDialogBuilder + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.warning) + .setCancelable(false).setMessage( + R.string.androidacy_test_mode_warning + ) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + // User clicked OK button + getSharedPreferences("mmm")!! + .edit().putBoolean("androidacy_test_mode", true).apply() + // Check the switch + val mStartActivity = + Intent(requireContext(), MainActivity::class.java) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + // If < 23, FLAG_IMMUTABLE is not available + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = + requireContext().getSystemService(ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = + mPendingIntent + Timber.d( + "Restarting app to save staging endpoint preference: %s", + newValue + ) + exitProcess(0) // Exit app process + } + .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> + // User cancelled the dialog + // Uncheck the switch + val switchPreferenceCompat = + androidacyTestMode as SwitchPreferenceCompat + switchPreferenceCompat.isChecked = false + // There's probably a better way to do this than duplicate code but I'm too lazy to figure it out + getSharedPreferences("mmm")!! + .edit().putBoolean("androidacy_test_mode", false).apply() + }.show() + } else { + getSharedPreferences("mmm")!! + .edit().putBoolean("androidacy_test_mode", false).apply() + // Show dialog to restart app with ok button + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.warning) + .setCancelable(false).setMessage( + R.string.androidacy_test_mode_disable_warning + ) + .setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + // User clicked OK button + val mStartActivity = + Intent(requireContext(), MainActivity::class.java) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + // If < 23, FLAG_IMMUTABLE is not available + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = + requireContext().getSystemService(ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = + mPendingIntent + Timber.d( + "Restarting app to save staging endpoint preference: %s", + newValue + ) + exitProcess(0) // Exit app process + }.show() + } + true + } + } + // Get magisk_alt_repo enabled state from room reposlist db + val db = databaseBuilder( + requireContext(), + ReposListDatabase::class.java, + "ReposList.db" + ).allowMainThreadQueries().build() + + // add listener to magisk_alt_repo_enabled switch to update room db + val magiskAltRepoEnabled = + findPreference("pref_magisk_alt_repo_enabled")!! + magiskAltRepoEnabled.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + // Update room db + db.reposListDao().setEnabled( + "magisk_alt_repo", + java.lang.Boolean.parseBoolean(newValue.toString()) + ) + true + } + // Disable toggling the pref_androidacy_repo_enabled on builds without an + // ANDROIDACY_CLIENT_ID or where the ANDROIDACY_CLIENT_ID is empty + val androidacyRepoEnabled = + findPreference("pref_androidacy_repo_enabled")!! + if (BuildConfig.ANDROIDACY_CLIENT_ID == "") { + androidacyRepoEnabled.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.androidacy_repo_disabled) + .setCancelable(false).setMessage( + R.string.androidacy_repo_disabled_message + ) + .setPositiveButton(R.string.download_full_app) { _: DialogInterface?, _: Int -> + // User clicked OK button. Open GitHub releases page + val browserIntent = 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.isChecked = false + // Disable in room db + db.reposListDao().setEnabled("androidacy_repo", false) + false + } + } else { + // get if androidacy repo is enabled from room db + val (_, _, androidacyRepoEnabledPref) = db.reposListDao().getById("androidacy_repo") + // set the switch to the current state + androidacyRepoEnabled.isChecked = androidacyRepoEnabledPref + // add a click listener to the switch + androidacyRepoEnabled.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val enabled = androidacyRepoEnabled.isChecked + // save the new state + db.reposListDao().setEnabled("androidacy_repo", enabled) + true + } + if (androidacyRepoEnabledPref) { + // get user role from AndroidacyRepoData.userInfo + val userInfo = AndroidacyRepoData.instance.userInfo + if (userInfo != null) { + val userRole = userInfo[0][1] + if (Objects.nonNull(userRole) && userRole != "Guest") { + // Disable the pref_androidacy_repo_api_donate preference + val prefAndroidacyRepoApiD = + findPreference("pref_androidacy_repo_donate")!! + prefAndroidacyRepoApiD.isEnabled = false + prefAndroidacyRepoApiD.setSummary(R.string.upgraded_summary) + prefAndroidacyRepoApiD.setTitle(R.string.upgraded) + prefAndroidacyRepoApiD.setIcon(R.drawable.baseline_check_24) + } else if (BuildConfig.FLAVOR == "play") { + // Disable the pref_androidacy_repo_api_token preference and hide the donate button + val prefAndroidacyRepoApiD = + findPreference("pref_androidacy_repo_donate")!! + prefAndroidacyRepoApiD.isEnabled = false + prefAndroidacyRepoApiD.isVisible = false + } + } + val originalApiKeyRef = arrayOf( + 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 + val prefAndroidacyRepoApiKey = + 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.text = originalApiKeyRef[0] + prefAndroidacyRepoApiKey.isVisible = true + prefAndroidacyRepoApiKey.setOnBindEditTextListener { editText: EditText -> + editText.setSingleLine() + // Make the single line wrap + editText.setHorizontallyScrolling(false) + // Set the height to the maximum required to fit the text + editText.maxLines = Int.MAX_VALUE + // Make ok button say "Save" + editText.imeOptions = EditorInfo.IME_ACTION_DONE + } + prefAndroidacyRepoApiKey.setPositiveButtonText(R.string.save_api_key) + prefAndroidacyRepoApiKey.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener setOnPreferenceChangeListener@{ _: Preference?, newValue: Any -> + // 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}".toRegex())) { + // Show snack bar with error + Snackbar.make( + requireView(), + R.string.api_key_mismatch, + BaseTransientBottomBar.LENGTH_LONG + ).show() + // Restore the original api key + prefAndroidacyRepoApiKey.text = originalApiKeyRef[0] + prefAndroidacyRepoApiKey.performClick() + return@setOnPreferenceChangeListener false + } + // Make sure originalApiKeyRef is not null + if (originalApiKeyRef[0] == newValue) return@setOnPreferenceChangeListener true + // get original api key + val apiKey = newValue.toString() + // Show snack bar with indeterminate progress + Snackbar.make( + requireView(), + R.string.checking_api_key, + BaseTransientBottomBar.LENGTH_INDEFINITE + ).setAction( + R.string.cancel + ) { + // Restore the original api key + prefAndroidacyRepoApiKey.text = originalApiKeyRef[0] + }.show() + // Check the API key on a background thread + Thread(Runnable { + // If key is empty, just remove it and change the text of the snack bar + if (apiKey.isEmpty()) { + getSharedPreferences("androidacy")!!.edit() + .remove("pref_androidacy_api_token").apply() + Handler(Looper.getMainLooper()).post { + Snackbar.make( + requireView(), + R.string.api_key_removed, + BaseTransientBottomBar.LENGTH_SHORT + ).show() + // Show dialog to restart app with ok button + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.restart) + .setCancelable(false).setMessage( + R.string.api_key_restart + ) + .setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + // User clicked OK button + val mStartActivity = Intent( + requireContext(), + MainActivity::class.java + ) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + // If < 23, FLAG_IMMUTABLE is not available + val mPendingIntent: PendingIntent = PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = + requireContext().getSystemService(ALARM_SERVICE) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = + mPendingIntent + Timber.d( + "Restarting app to save token preference: %s", + newValue + ) + exitProcess(0) // Exit app process + }.show() + } + } else { + // If key < 64 chars, it's not valid + if (apiKey.length < 64) { + Handler(Looper.getMainLooper()).post { + Snackbar.make( + requireView(), + R.string.api_key_invalid, + BaseTransientBottomBar.LENGTH_SHORT + ).show() + // Save the original key + getSharedPreferences("androidacy")!! + .edit().putString( + "pref_androidacy_api_token", + originalApiKeyRef[0] + ).apply() + // Re-show the dialog with an error + prefAndroidacyRepoApiKey.performClick() + // Show error + prefAndroidacyRepoApiKey.dialogMessage = + getString(R.string.api_key_invalid) + } + } else { + // If the key is the same as the original, just show a snack bar + if (apiKey == originalApiKeyRef[0]) { + Handler(Looper.getMainLooper()).post { + Snackbar.make( + requireView(), + R.string.api_key_unchanged, + BaseTransientBottomBar.LENGTH_SHORT + ).show() + } + return@Runnable + } + var valid = false + try { + valid = AndroidacyRepoData.instance.isValidToken(apiKey) + } catch (ignored: IOException) { + } + // If the key is valid, save it + if (valid) { + originalApiKeyRef[0] = apiKey + getINSTANCE()!!.androidacyRepoData!!.setToken(apiKey) + getSharedPreferences("androidacy")!! + .edit() + .putString("pref_androidacy_api_token", apiKey) + .apply() + // Snackbar with success and restart button + Handler(Looper.getMainLooper()).post { + Snackbar.make( + requireView(), + R.string.api_key_valid, + BaseTransientBottomBar.LENGTH_SHORT + ).show() + // Show dialog to restart app with ok button + MaterialAlertDialogBuilder(requireContext()).setTitle( + R.string.restart + ).setCancelable(false).setMessage( + R.string.api_key_restart + ) + .setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + // User clicked OK button + val mStartActivity = Intent( + requireContext(), + MainActivity::class.java + ) + mStartActivity.flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val mPendingIntentId = 123456 + // If < 23, FLAG_IMMUTABLE is not available + val mPendingIntent: PendingIntent = + PendingIntent.getActivity( + requireContext(), + mPendingIntentId, + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val mgr = requireContext().getSystemService( + ALARM_SERVICE + ) as AlarmManager + mgr[AlarmManager.RTC, System.currentTimeMillis() + 100] = + mPendingIntent + Timber.d( + "Restarting app to save token preference: %s", + newValue + ) + exitProcess(0) // Exit app process + }.show() + } + } else { + Handler(Looper.getMainLooper()).post { + Snackbar.make( + requireView(), + R.string.api_key_invalid, + BaseTransientBottomBar.LENGTH_SHORT + ).show() + // Save the original key + INSTANCE!!.getSharedPreferences("androidacy", 0) + .edit().putString( + "pref_androidacy_api_token", + originalApiKeyRef[0] + ).apply() + // Re-show the dialog with an error + prefAndroidacyRepoApiKey.performClick() + // Show error + prefAndroidacyRepoApiKey.dialogMessage = + getString(R.string.api_key_invalid) + } + } + } + } + }).start() + true + } + } + } + } + + @SuppressLint("RestrictedApi") + fun updateCustomRepoList(initial: Boolean) { + // get all repos that are not built-in + var custRepoEntries = 0 + // array of custom repos + val customRepos = ArrayList() + val db = databaseBuilder( + requireContext(), + ReposListDatabase::class.java, + "ReposList.db" + ).allowMainThreadQueries().build() + val reposList = db.reposListDao().getAll() + for ((id) in reposList) { + val buildInRepos = ArrayList(mutableListOf("androidacy_repo", "magisk_alt_repo")) + if (!buildInRepos.contains(id)) { + custRepoEntries++ + customRepos.add(id) + } + } + Timber.d("%d repos: %s", custRepoEntries, customRepos) + val customRepoManager = getINSTANCE()!!.customRepoManager + for (i in 0 until custRepoEntries) { + // get the id of the repo at current index in customRepos + val repoData = customRepoManager!!.getRepo(customRepos[i]) + // 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) { + val preference = findPreference("pref_custom_repo_" + i + "_delete") + ?: continue + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { preference1: Preference -> + db.reposListDao().delete(customRepos[i]) + customRepoManager.removeRepo(i) + updateCustomRepoList(false) + preference1.isVisible = false + 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 (custRepoEntries == 0) { + for (i in 0..4) { + val preference = findPreference("pref_custom_repo_$i") + ?: continue + preference.isVisible = false + } + } else { + for (i in 0..4) { + val preference = findPreference("pref_custom_repo_$i") + ?: continue + if (i >= custRepoEntries) { + preference.isVisible = false + } + } + } + var preference = findPreference("pref_custom_add_repo") ?: return + preference.isVisible = + customRepoManager!!.canAddRepo() && customRepoManager.repoCount < 5 + if (initial) { // Custom repo add button part. + preference = findPreference("pref_custom_add_repo_button")!! + if (preference == null) return + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val context = requireContext() + val builder = MaterialAlertDialogBuilder(context) + val input = EditText(context) + input.setHint(R.string.custom_url) + input.setHorizontallyScrolling(true) + input.maxLines = 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") { _: DialogInterface?, _: Int -> + var text = input.text.toString() + text = text.trim { it <= ' ' } + // string should not be empty, start with https://, and not contain any spaces. http links are not allowed. + if (text.matches("^https://.*".toRegex()) && !text.contains(" ") && text.isNotEmpty()) { + if (customRepoManager.canAddRepo(text)) { + val customRepoData = customRepoManager.addRepo(text) + object : Thread("Add Custom Repo Thread") { + override fun run() { + try { + customRepoData!!.quickPrePopulate() + UiThreadHandler.handler.post { + updateCustomRepoList( + false + ) + } + } catch (e: Exception) { + Timber.e(e) + // show new dialog + Handler(Looper.getMainLooper()).post { + MaterialAlertDialogBuilder(context).setTitle( + R.string.error_adding + ).setMessage(e.message) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> } + .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: DialogInterface, _: Int -> dialog.cancel() } + builder.setNeutralButton("Docs") { _: DialogInterface?, _: Int -> + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://github.com/Androidacy/MagiskModuleManager/blob/master/docs/DEVELOPERS.md#custom-repo-format") + ) + startActivity(intent) + } + val alertDialog = builder.show() + val positiveButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE) + // validate as they type + input.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + charSequence: CharSequence, + start: Int, + before: Int, + count: Int + ) { + 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.error = getString(R.string.empty_field) + Timber.d("No input for repo") + positiveButton.isEnabled = false + } else if (!charSequence.toString() + .matches("^https://.*".toRegex()) + ) { + input.error = getString(R.string.invalid_repo_url) + Timber.d("Non https link for repo") + positiveButton.isEnabled = false + } else if (charSequence.toString().contains(" ")) { + input.error = getString(R.string.invalid_repo_url) + Timber.d("Repo url has space") + positiveButton.isEnabled = false + } else if (!customRepoManager.canAddRepo(charSequence.toString())) { + input.error = getString(R.string.repo_already_added) + Timber.d("Could not add repo for misc reason") + positiveButton.isEnabled = false + } else { + // enable ok button + Timber.d("Repo URL is ok") + positiveButton.isEnabled = true + } + } + + override fun afterTextChanged(s: Editable) {} + }) + positiveButton.isEnabled = false + val dp10 = FoxDisplay.dpToPixel(10f) + val dp20 = FoxDisplay.dpToPixel(20f) + FoxViewCompat.setMargin(input, dp20, dp10, dp20, dp10) + true + } + } + } + + private fun setRepoData(url: String) { + val repoData = getINSTANCE()!![url] + setRepoData( + repoData, + "pref_" + if (repoData == null) internalIdOfUrl(url) else repoData.preferenceId + ) + } + + private fun setRepoData(repoData: RepoData?, preferenceName: String) { + if (repoData == null) return + Timber.d("Setting preference $preferenceName to $repoData") + val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + var preference = findPreference(preferenceName) ?: return + if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) { + if (repoData != null) { + val db = databaseBuilder( + requireContext(), + ReposListDatabase::class.java, + "ReposList.db" + ).allowMainThreadQueries().build() + val reposList = db.reposListDao().getById(repoData.preferenceId!!) + Timber.d("Setting preference $preferenceName because it is not the Androidacy repo or the Magisk Alt Repo") + if (repoData.isForceHide || reposList == null) { + 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, + reposList + ) + preference.title = repoData.name + preference.isVisible = true + // set website, support, and submitmodule as well as donate + if (repoData.getWebsite() != null) { + findPreference(preferenceName + "_website")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), repoData.getWebsite()) + true + } + } else { + findPreference(preferenceName + "_website")!!.isVisible = + false + } + if (repoData.getSupport() != null) { + findPreference(preferenceName + "_support")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), repoData.getSupport()) + true + } + } else { + findPreference(preferenceName + "_support")!!.isVisible = + false + } + if (repoData.getSubmitModule() != null) { + findPreference(preferenceName + "_submit")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), repoData.getSubmitModule()) + true + } + } else { + findPreference(preferenceName + "_submit")!!.isVisible = + false + } + if (repoData.getDonate() != null) { + findPreference(preferenceName + "_donate")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), repoData.getDonate()) + true + } + } else { + findPreference(preferenceName + "_donate")!!.isVisible = + false + } + } + } else { + Timber.d("Hiding preference $preferenceName because it's data is null") + hideRepoData(preferenceName) + return + } + } + preference = findPreference(preferenceName + "_enabled") ?: return + if (preference != null) { + // Handle custom repo separately + if (repoData is CustomRepoData) { + preference.setTitle(R.string.custom_repo_always_on) + // Disable the preference + preference.isEnabled = false + return + } else { + (preference as TwoStatePreference).isChecked = repoData.isEnabled + preference.setTitle(if (repoData.isEnabled) R.string.repo_enabled else R.string.repo_disabled) + preference.setOnPreferenceChangeListener { p: Preference, newValue: Any -> + p.setTitle(if (newValue as Boolean) R.string.repo_enabled else 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() + true + } + } + } + preference = findPreference(preferenceName + "_website") ?: return + val homepage = repoData.getWebsite() + if (preference != null) { + if (homepage.isNotEmpty()) { + preference.isVisible = true + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), homepage) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + OnPreferenceLongClickListener { + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip(ClipData.newPlainText(toastText, homepage)) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + } else { + preference.isVisible = false + } + } + preference = findPreference(preferenceName + "_support") ?: return + val supportUrl = repoData.getSupport() + if (preference != null) { + if (!supportUrl.isNullOrEmpty()) { + preference.isVisible = true + preference.setIcon(supportIconForUrl(supportUrl)) + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), supportUrl) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + OnPreferenceLongClickListener { + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip(ClipData.newPlainText(toastText, supportUrl)) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + } else { + preference.isVisible = false + } + } + preference = findPreference(preferenceName + "_donate") ?: return + val donateUrl = repoData.getDonate() + if (preference != null) { + if (donateUrl != null) { + preference.isVisible = true + preference.setIcon(donateIconForUrl(donateUrl)) + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), donateUrl) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + OnPreferenceLongClickListener { + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip(ClipData.newPlainText(toastText, donateUrl)) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + } else { + preference.isVisible = false + } + } + preference = findPreference(preferenceName + "_submit") ?: return + val submissionUrl = repoData.getSubmitModule() + if (preference != null) { + if (!submissionUrl.isNullOrEmpty()) { + preference.isVisible = true + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + openUrl(getFoxActivity(this), submissionUrl) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + OnPreferenceLongClickListener { + val toastText = requireContext().getString(R.string.link_copied) + clipboard.setPrimaryClip( + ClipData.newPlainText( + toastText, + submissionUrl + ) + ) + Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show() + true + } + } else { + preference.isVisible = false + } + } + } + + private fun hideRepoData(preferenceName: String) { + val preference = findPreference(preferenceName) ?: return + preference.isVisible = false + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.sharedPreferencesName = "mmm" + setPreferencesFromResource(R.xml.repo_preferences, rootKey) + applyMaterial3(preferenceScreen) + setRepoData(RepoManager.MAGISK_ALT_REPO) + setRepoData(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT) + updateCustomRepoList(true) + onCreatePreferencesAndroidacy() + } + + companion object { + /** + * *says proudly*: I stole it + * + * + * namely, from [neo wellbeing](https://github.com/NeoApplications/Neo-Wellbeing/blob/9fca4136263780c022f9ec6433c0b43d159166db/app/src/main/java/org/eu/droid_ng/wellbeing/prefs/SettingsActivity.java#L101) + */ + fun applyMaterial3(p: Preference) { + if (p is PreferenceGroup) { + for (i in 0 until p.preferenceCount) { + applyMaterial3(p.getPreference(i)) + } + } + (p as? SwitchPreferenceCompat)?.widgetLayoutResource = + R.layout.preference_material_switch + } + } + } + + @Suppress("MemberVisibilityCanBePrivate") + companion object { + // Shamelessly adapted from https://github.com/DrKLO/Telegram/blob/2c71f6c92b45386f0c2b25f1442596462404bb39/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java#L1254 + const val PERFORMANCE_CLASS_LOW = 0 + const val PERFORMANCE_CLASS_AVERAGE = 1 + const val PERFORMANCE_CLASS_HIGH = 2 + private const val LANGUAGE_SUPPORT_LEVEL = 1 + private var devModeStepFirstBootIgnore = isDeveloper + private var devModeStep = 0 + + @get:PerformanceClass + val devicePerformanceClass: Int + get() { + // special algorithm to determine performance class. low is < 4 cores and/ore < 4GB ram, mid is 4-6 cores and 4-6GB ram, high is > 6 cores and > 6GB ram. android sdk version is used as well + // device is awarded 1 point for each core and 1 point for each GB of ram. + var points = 0 + val cores = Runtime.getRuntime().availableProcessors() + val activityManager = INSTANCE!!.getSystemService( + ACTIVITY_SERVICE + ) as ActivityManager + if (activityManager != null) { + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + val totalMemory = memoryInfo.totalMem + points += cores + points += (totalMemory / 1024 / 1024 / 1024).toInt() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + points += 1 + } + Timber.d("Device performance class: %d", points) + return if (points <= 7) { + PERFORMANCE_CLASS_LOW + } else if (points <= 12) { + PERFORMANCE_CLASS_AVERAGE + } else { + PERFORMANCE_CLASS_HIGH + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57fc253..1bb17d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -441,4 +441,5 @@ EULA and Terms Could not create repos db Failed to create module cache db + Device is not compatible with blur diff --git a/gradle.properties b/gradle.properties index f9cf800..a846a69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,3 @@ -# -# 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. -# - ## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # @@ -14,7 +10,7 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -#Mon May 29 11:36:28 EDT 2023 +#Mon Jun 19 16:29:35 EDT 2023 android.defaults.buildfeatures.buildconfig=true android.enableJetifier=false android.enableR8.fullMode=true @@ -22,5 +18,5 @@ android.useAndroidX=true org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn -org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M" -Dfile.encoding\=UTF-8 -XX\:+UseParallelGC -XX\:ReservedCodeCacheSize\=768m +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8 -XX\:+UseParallelGC -XX\:ReservedCodeCacheSize\=768m org.gradle.parallel=true