diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a278175..834d1bf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -119,6 +119,9 @@ android { renderscriptOptimLevel = 3 signingConfig = signingConfigs.getByName("release") multiDexEnabled = true + isDebuggable = false + isJniDebuggable = false + isRenderscriptDebuggable = false } getByName("debug") { applicationIdSuffix = ".debug" @@ -462,7 +465,7 @@ dependencies { implementation("com.github.KieronQuinn:MonetCompat:0.4.1") implementation("com.github.Fox2Code.FoxCompat:foxcompat:1.2.14") implementation("com.github.Fox2Code.FoxCompat:hiddenapis:1.2.14") - implementation("com.mikepenz:aboutlibraries:10.8.2") + implementation("com.mikepenz:aboutlibraries:10.8.3") // Utils implementation("androidx.work:work-runtime:2.8.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 652e05e..bfd9e42 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -162,7 +162,9 @@ + android:exported="false" + android:parentActivityName=".settings.SettingsActivity" + android:theme="@style/Theme.MagiskModuleManager.NoActionBar" /> ("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 = + Preference.SummaryProvider { _: Preference? -> themePreference.entry } + themePreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + 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 + UiThreadHandler.handler.postDelayed({ + MainApplication.INSTANCE!!.updateTheme() + FoxActivity.getFoxActivity(this) + .setThemeRecreate(MainApplication.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 + }.show() + } else { + findPreference("pref_enable_monet")!!.isEnabled = true + findPreference("pref_enable_monet")?.summary = "" + findPreference("pref_enable_blur")!!.isEnabled = true + findPreference("pref_enable_blur")?.summary = "" + } + UiThreadHandler.handler.postDelayed({ + MainApplication.INSTANCE!!.updateTheme() + FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.INSTANCE!!.getManagerThemeResId()) + }, 1) + 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({ + MainApplication.INSTANCE!!.updateTheme() + (requireActivity() as FoxActivity).setThemeRecreate(MainApplication.INSTANCE!!.getManagerThemeResId()) + }, 1) + true + } + + + val enableBlur = findPreference("pref_enable_blur") + // Disable blur on low performance devices + if (SettingsActivity.devicePerformanceClass < SettingsActivity.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 + } + } + + + // 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 + } + + + val languageSelector = findPreference("pref_language_selector") + languageSelector!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + val ls = LanguageSwitcher( + requireActivity() + ) + ls.setSupportedStringLocales(MainApplication.supportedLocales) + ls.showChangeLanguageDialog(activity) + true + } + + // Long click to copy url + languageSelectorCta.onPreferenceLongClickListener = + LongClickablePreference.OnPreferenceLongClickListener { _: Preference? -> + val clipboard = + requireContext().getSystemService(FoxActivity.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 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/CreditsFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/CreditsFragment.kt new file mode 100644 index 0000000..f639577 --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/CreditsFragment.kt @@ -0,0 +1,123 @@ +package com.fox2code.mmm.settings + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.utils.IntentHelper +import timber.log.Timber + +class CreditsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + + val name = "mmmx" + val context: Context? = MainApplication.INSTANCE + val masterKey: MasterKey + val preferenceManager = preferenceManager + val dataStore: SharedPreferenceDataStore + 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" + } catch (e: Exception) { + Timber.e(e, "Failed to create encrypted shared preferences") + throw RuntimeException(getString(R.string.error_encrypted_shared_preferences)) + } + + setPreferencesFromResource(R.xml.credits_preferences, rootKey) + + + + val clipboard = requireContext().getSystemService(FoxActivity.CLIPBOARD_SERVICE) as ClipboardManager + + // pref_contributors should lead to the contributors page + var linkClickable: LongClickablePreference? = 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" + IntentHelper.openUrl(p.context, url) + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 + } + + + // Next, the pref_androidacy_thanks should lead to the androidacy website + linkClickable = findPreference("pref_androidacy_thanks") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + IntentHelper.openUrl( + p.context, + "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager" + ) + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 -> + IntentHelper.openUrl(p.context, "https://github.com/Fox2Code") + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/DebugFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/DebugFragment.kt new file mode 100644 index 0000000..d53ddef --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/DebugFragment.kt @@ -0,0 +1,200 @@ +package com.fox2code.mmm.settings + +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.Constants +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.installer.InstallerInitializer +import com.fox2code.mmm.utils.sentry.SentryMain +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.apache.commons.io.FileUtils +import timber.log.Timber +import java.io.BufferedReader +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader + +class DebugFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val name = "mmmx" + val context: Context? = MainApplication.INSTANCE + val masterKey: MasterKey + val preferenceManager = preferenceManager + val dataStore: SharedPreferenceDataStore + 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" + } catch (e: Exception) { + Timber.e(e, "Failed to create encrypted shared preferences") + throw RuntimeException(getString(R.string.error_encrypted_shared_preferences)) + } + setPreferencesFromResource(R.xml.debugging_preferences, rootKey) + + + if (!MainApplication.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(MainApplication.INSTANCE!!.dataDir.toString() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm")) + FileUtils.forceMkdir(File(MainApplication.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 || InstallerInitializer.peekMagiskPath() == null) { + // Hide the pref_crash option if not in debug mode - stop users from purposely crashing the app + Timber.i(InstallerInitializer.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 + MainApplication.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 (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND || !MainApplication.isDeveloper) { + findPreference("pref_use_magisk_install_command")!!.isVisible = false + } + + + // 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, some device and app info: namely device oem and model, android version and build, app version and build + fileOutputStream.write( + String.format( + "Device: %s %s\nAndroid Version: %s\nROM: %s\nApp Version: %s (%s)\n\n", + Build.MANUFACTURER, + Build.MODEL, + Build.VERSION.RELEASE, + Build.FINGERPRINT, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE + ).toByteArray() + ) + // next, the logs + // get our logs from logcat + val process = Runtime.getRuntime().exec("logcat -d") + val bufferedReader = BufferedReader( + InputStreamReader(process.inputStream) + ) + var line: String? + val iterator: Iterator = bufferedReader.lines().iterator() + while (iterator.hasNext()) { + line = iterator.next() + fileOutputStream.write(line.toByteArray()) + fileOutputStream.write("\n".toByteArray()) + } + fileOutputStream.flush() + Toast.makeText( + requireContext(), R.string.logs_saved, Toast.LENGTH_SHORT + ).show() + } 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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/InfoFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/InfoFragment.kt new file mode 100644 index 0000000..15faa1d --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/InfoFragment.kt @@ -0,0 +1,219 @@ +package com.fox2code.mmm.settings + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.androidacy.AndroidacyRepoData +import com.fox2code.mmm.utils.IntentHelper +import timber.log.Timber + +@Suppress("KotlinConstantConditions") +class InfoFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val name = "mmmx" + val context: Context? = MainApplication.INSTANCE + val masterKey: MasterKey + val preferenceManager = preferenceManager + val dataStore: SharedPreferenceDataStore + 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" + } catch (e: Exception) { + Timber.e(e, "Failed to create encrypted shared preferences") + throw RuntimeException(getString(R.string.error_encrypted_shared_preferences)) + } + + setPreferencesFromResource(R.xml.app_info_preferences, rootKey) + + val clipboard = + requireContext().getSystemService(FoxActivity.CLIPBOARD_SERVICE) as ClipboardManager + var linkClickable: LongClickablePreference? + if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) { + linkClickable = findPreference("pref_report_bug") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + IntentHelper.openUrl( + p.context, "https://github.com/Androidacy/MagiskModuleManager/issues" + ) + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 -> + // build url from BuildConfig.REMOTE_URL and BuildConfig.COMMIT_HASH. May have to remove the .git at the end + IntentHelper.openUrl( + p.context, + finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH + ) + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 + } + + + val prefDonateFox = findPreference("pref_donate_fox") + if (BuildConfig.FLAVOR != "play") { + prefDonateFox!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // open fox + IntentHelper.openUrl( + FoxActivity.getFoxActivity(this), "https://paypal.me/fox2code" + ) + true + } + // handle long click on pref_donate_fox + prefDonateFox.onPreferenceLongClickListener = + LongClickablePreference.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 + IntentHelper.openUrl( + FoxActivity.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 = + LongClickablePreference.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 + } + + + linkClickable = findPreference("pref_support") + linkClickable!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { p: Preference -> + IntentHelper.openUrl(p.context, "https://t.me/Fox2Code_Chat") + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 -> + IntentHelper.openUrl(p.context, "https://t.me/androidacy") + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt new file mode 100644 index 0000000..8305c38 --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/PrivacyFragment.kt @@ -0,0 +1,89 @@ +package com.fox2code.mmm.settings + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.MainActivity +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.utils.sentry.SentryMain +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import timber.log.Timber +import kotlin.system.exitProcess + +class PrivacyFragment : PreferenceFragmentCompat() { + @SuppressLint("CommitPrefEdits") + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val name = "mmmx" + val context: Context? = MainApplication.INSTANCE + val masterKey: MasterKey + val preferenceManager = preferenceManager + val dataStore: SharedPreferenceDataStore + 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" + } catch (e: Exception) { + Timber.e(e, "Failed to create encrypted shared preferences") + throw RuntimeException(getString(R.string.error_encrypted_shared_preferences)) + } + setPreferencesFromResource(R.xml.privacy_preferences, rootKey) + // Crash reporting + val crashReportingPreference = + findPreference("pref_crash_reporting") + if (!SentryMain.IS_SENTRY_INSTALLED) crashReportingPreference!!.isVisible = false + crashReportingPreference!!.isChecked = MainApplication.isCrashReportingEnabled + val initialValue: Any = MainApplication.isCrashReportingEnabled + crashReportingPreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener setOnPreferenceChangeListener@{ _: Preference?, newValue: Any -> + 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(FoxActivity.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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/RepoFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/RepoFragment.kt new file mode 100644 index 0000000..e87c4e8 --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/RepoFragment.kt @@ -0,0 +1,875 @@ +package com.fox2code.mmm.settings + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context.ALARM_SERVICE +import android.content.Context.CLIPBOARD_SERVICE +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.Editable +import android.text.TextWatcher +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.Toast +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroup +import androidx.preference.SwitchPreferenceCompat +import androidx.preference.TwoStatePreference +import androidx.room.Room +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.foxcompat.view.FoxDisplay +import com.fox2code.foxcompat.view.FoxViewCompat +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.MainActivity +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.androidacy.AndroidacyRepoData +import com.fox2code.mmm.module.ActionButtonType +import com.fox2code.mmm.repo.CustomRepoData +import com.fox2code.mmm.repo.RepoData +import com.fox2code.mmm.repo.RepoManager +import com.fox2code.mmm.utils.IntentHelper +import com.fox2code.mmm.utils.room.ReposListDatabase +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import com.topjohnwu.superuser.internal.UiThreadHandler +import timber.log.Timber +import java.io.IOException +import java.util.Objects +import kotlin.system.exitProcess + +@Suppress("SENSELESS_COMPARISON") +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 (!MainApplication.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 + MainApplication.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 + 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 + 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 = Room.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()) + ) + MainApplication.INSTANCE!!.repoModules.clear() + 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) + MainApplication.INSTANCE!!.repoModules.clear() + 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( + 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 + 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()) { + MainApplication.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 + MainApplication.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 + RepoManager.getINSTANCE()!!.androidacyRepoData!!.setToken(apiKey) + MainApplication.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 + MainApplication.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 = Room.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 = RepoManager.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 = RepoManager.getINSTANCE()!![url] + setRepoData( + repoData, + "pref_" + if (repoData == null) RepoManager.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 = Room.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 { + IntentHelper.openUrl( + FoxActivity.getFoxActivity(this), + repoData.getWebsite() + ) + true + } + } else { + findPreference(preferenceName + "_website")!!.isVisible = + false + } + if (repoData.getSupport() != null) { + findPreference(preferenceName + "_support")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + IntentHelper.openUrl( + FoxActivity.getFoxActivity(this), + repoData.getSupport() + ) + true + } + } else { + findPreference("${preferenceName}_support")!!.isVisible = + false + } + if (repoData.getSubmitModule() != null) { + findPreference(preferenceName + "_submit")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + IntentHelper.openUrl( + FoxActivity.getFoxActivity(this), + repoData.getSubmitModule() + ) + true + } + } else { + findPreference(preferenceName + "_submit")!!.isVisible = + false + } + if (repoData.getDonate() != null) { + findPreference(preferenceName + "_donate")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + IntentHelper.openUrl( + FoxActivity.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() + MainApplication.INSTANCE!!.repoModules.clear() + true + } + } + } + preference = findPreference(preferenceName + "_website") ?: return + val homepage = repoData.getWebsite() + if (preference != null) { + if (homepage.isNotEmpty()) { + preference.isVisible = true + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + IntentHelper.openUrl(FoxActivity.getFoxActivity(this), homepage) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + LongClickablePreference.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(ActionButtonType.supportIconForUrl(supportUrl)) + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + IntentHelper.openUrl(FoxActivity.getFoxActivity(this), supportUrl) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + LongClickablePreference.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(ActionButtonType.donateIconForUrl(donateUrl)) + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + IntentHelper.openUrl(FoxActivity.getFoxActivity(this), donateUrl) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + LongClickablePreference.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 { + IntentHelper.openUrl(FoxActivity.getFoxActivity(this), submissionUrl) + true + } + (preference as LongClickablePreference).onPreferenceLongClickListener = + LongClickablePreference.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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/SecurityFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/SecurityFragment.kt new file mode 100644 index 0000000..443a9ad --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/SecurityFragment.kt @@ -0,0 +1,121 @@ +package com.fox2code.mmm.settings + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.MainActivity +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.utils.io.net.Http +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import timber.log.Timber +import kotlin.system.exitProcess + +class SecurityFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val name = "mmmx" + val context: Context? = MainApplication.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.security_preferences, rootKey) + + findPreference("pref_dns_over_https")!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, v: Any? -> + Http.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(FoxActivity.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(FoxActivity.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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt index edb27d7..6dabef2 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.kt @@ -5,110 +5,42 @@ 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.ExpiredActivity 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.sql.Timestamp -import java.util.Objects -import java.util.Random -import kotlin.system.exitProcess @Suppress("SENSELESS_COMPARISON") class SettingsActivity : FoxActivity(), LanguageActivity { + lateinit var sharedPreferences: SharedPreferences + @SuppressLint("RestrictedApi") private val onItemSelectedListener = NavigationBarView.OnItemSelectedListener { item: MenuItem -> @@ -121,6 +53,7 @@ class SettingsActivity : FoxActivity(), LanguageActivity { startActivity(intent) return@OnItemSelectedListener true } + R.id.online_menu_item -> { val intent = Intent(this, MainActivity::class.java) intent.action = "android.intent.action.SHOW_ONLINE" @@ -128,16 +61,30 @@ class SettingsActivity : FoxActivity(), LanguageActivity { startActivity(intent) return@OnItemSelectedListener true } + else -> { return@OnItemSelectedListener false } } } + @SuppressLint("RestrictedApi") override fun onCreate(savedInstanceState: Bundle?) { devModeStep = 0 super.onCreate(savedInstanceState) + + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { preferenceFragmentCompat: PreferenceFragmentCompat, preference: Preference -> + val fragment = supportFragmentManager.fragmentFactory.instantiate( + classLoader, preference.fragment.toString() + ) + fragment.arguments = preference.extras + @Suppress("DEPRECATION") fragment.setTargetFragment(preferenceFragmentCompat, 0) + supportFragmentManager.beginTransaction().replace(R.id.settings, fragment) + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).addToBackStack(null) + .commit() + true + } TrackHelper.track().screen(this).with(INSTANCE!!.getTracker()) setContentView(R.layout.settings_activity) setTitle(R.string.app_name_v2) @@ -170,6 +117,7 @@ class SettingsActivity : FoxActivity(), LanguageActivity { supportFragmentManager.beginTransaction().replace(R.id.settings, settingsFragment) .commit() } + } @SuppressLint("InlinedApi") @@ -183,7 +131,7 @@ class SettingsActivity : FoxActivity(), LanguageActivity { } annotation class PerformanceClass - class SettingsFragment : PreferenceFragmentCompat(), OnBackPressedCallback { + class SettingsFragment : PreferenceFragmentCompat() { @SuppressLint("UnspecifiedImmutableFlag") override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { val name = "mmmx" @@ -215,7 +163,7 @@ class SettingsActivity : FoxActivity(), LanguageActivity { setPreferencesFromResource(R.xml.root_preferences, rootKey) RepoFragment.applyMaterial3(preferenceScreen) // track all non empty values - val sharedPreferences = dataStore.sharedPreferences + dataStore.sharedPreferences // disabled until EncryptedSharedPreferences fixes getAll() // add bottom navigation bar to the settings val bottomNavigationView = @@ -224,932 +172,9 @@ class SettingsActivity : FoxActivity(), LanguageActivity { 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) + requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - // 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, some device and app info: namely device oem and model, android version and build, app version and build - fileOutputStream.write( - String.format( - "Device: %s %s\nAndroid Version: %s\nROM: %s\nApp Version: %s (%s)\n\n", - Build.MANUFACTURER, - Build.MODEL, - Build.VERSION.RELEASE, - Build.FINGERPRINT, - BuildConfig.VERSION_NAME, - BuildConfig.VERSION_CODE - ).toByteArray() - ) - // next, the logs - // get our logs from logcat - val process = Runtime.getRuntime().exec("logcat -d") - val bufferedReader = BufferedReader( - InputStreamReader(process.inputStream) - ) - var line: String? - val iterator: Iterator = bufferedReader.lines().iterator() - while (iterator.hasNext()) { - line = iterator.next() - fileOutputStream.write(line.toByteArray()) - fileOutputStream.write("\n".toByteArray()) - } - fileOutputStream.flush() - Toast.makeText( - requireContext(), - R.string.logs_saved, - Toast.LENGTH_SHORT - ).show() - } 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 @@ -1172,12 +197,9 @@ class SettingsActivity : FoxActivity(), LanguageActivity { 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, + R.string.keep_tapping_to_enter_hogwarts, Toast.LENGTH_SHORT ).show() } @@ -1185,914 +207,31 @@ class SettingsActivity : FoxActivity(), LanguageActivity { 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_v2) - 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 + // enable dev mode + editor.putBoolean("developer", true) + editor.apply() + // toast yer a wizard harry + if (versionClicks == 3) { + Toast.makeText( + p.context, + R.string.yer_a_wizard_harry, + Toast.LENGTH_LONG + ).show() } - } - // 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()) - ) - INSTANCE!!.repoModules.clear() 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) - INSTANCE!!.repoModules.clear() - 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() - INSTANCE!!.repoModules.clear() - 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 + // libslistener to fix that the libs view doesn't actually go away when user goes back + val libsBuilder = LibsBuilder().withShowLoadingProgress(true).withLicenseShown(true) + .withAboutMinimalDesign(false).withLicenseDialog(true).withVersionShown(true) + findPreference("pref_show_licenses")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // not correctly themed but less buggy than fragment + libsBuilder.start(requireContext()) + return@OnPreferenceClickListener true } - } } - 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") @@ -2101,8 +240,6 @@ class SettingsActivity : FoxActivity(), LanguageActivity { 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 diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/UpdateFragment.kt b/app/src/main/kotlin/com/fox2code/mmm/settings/UpdateFragment.kt new file mode 100644 index 0000000..f7f2561 --- /dev/null +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/UpdateFragment.kt @@ -0,0 +1,344 @@ +package com.fox2code.mmm.settings + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.text.InputType +import android.view.ViewGroup +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.Toast +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.AppUpdateManager +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.UpdateActivity +import com.fox2code.mmm.background.BackgroundUpdateChecker +import com.fox2code.mmm.manager.LocalModuleInfo +import com.fox2code.mmm.manager.ModuleManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textview.MaterialTextView +import timber.log.Timber +import java.util.Random + +class UpdateFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + + val name = "mmmx" + val context: Context? = MainApplication.INSTANCE + val masterKey: MasterKey + val preferenceManager = preferenceManager + val dataStore: SharedPreferenceDataStore + 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" + } catch (e: Exception) { + Timber.e(e, "Failed to create encrypted shared preferences") + throw RuntimeException(getString(R.string.error_encrypted_shared_preferences)) + } + setPreferencesFromResource(R.xml.update_preferences, rootKey) + RepoFragment.applyMaterial3(preferenceScreen) + // track all non empty values + val sharedPreferences = dataStore.sharedPreferences + 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 = MainApplication.isBackgroundUpdateCheckEnabled + debugNotification.isVisible = + MainApplication.isDeveloper && !MainApplication.isWrapped && MainApplication.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" + } + BackgroundUpdateChecker.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 (!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.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // set the box to unchecked + (backgroundUpdateCheck as SwitchPreferenceCompat?)!!.isChecked = false + // ensure that the preference is false + MainApplication.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 = + MainApplication.isBackgroundUpdateCheckEnabled && !MainApplication.isWrapped + backgroundUpdateCheck.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + val enabled = java.lang.Boolean.parseBoolean(newValue.toString()) + debugNotification.isEnabled = enabled + debugNotification.isVisible = + MainApplication.isDeveloper && !MainApplication.isWrapped && enabled + updateCheckExcludes.isEnabled = enabled + updateCheckExcludes.isVisible = enabled && !MainApplication.isWrapped + if (!enabled) { + BackgroundUpdateChecker.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 = + MainApplication.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 clipboard = + requireContext().getSystemService(FoxActivity.CLIPBOARD_SERVICE) as ClipboardManager + val linkClickable = findPreference("pref_update") + linkClickable!!.isVisible = + BuildConfig.ENABLE_AUTO_UPDATER && (BuildConfig.DEBUG || AppUpdateManager.appUpdateManager.peekHasUpdate()) + linkClickable.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + // open UpdateActivity with CHECK action + val intent = Intent(requireContext(), UpdateActivity::class.java) + intent.action = UpdateActivity.ACTIONS.CHECK.name + startActivity(intent) + true + } + linkClickable.onPreferenceLongClickListener = + LongClickablePreference.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 = + MainApplication.isDeveloper && MainApplication.isBackgroundUpdateCheckEnabled && !MainApplication.isWrapped + debugDownload.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + val intent = Intent(requireContext(), UpdateActivity::class.java) + intent.action = UpdateActivity.ACTIONS.DOWNLOAD.name + startActivity(intent) + true + } + } +} diff --git a/app/src/main/res/drawable/baseline_access_time_24.xml b/app/src/main/res/drawable/baseline_access_time_24.xml new file mode 100644 index 0000000..cf3ed0f --- /dev/null +++ b/app/src/main/res/drawable/baseline_access_time_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_people_24.xml b/app/src/main/res/drawable/baseline_people_24.xml new file mode 100644 index 0000000..fc90db1 --- /dev/null +++ b/app/src/main/res/drawable/baseline_people_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_privacy_tip_24.xml b/app/src/main/res/drawable/baseline_privacy_tip_24.xml new file mode 100644 index 0000000..7d053e8 --- /dev/null +++ b/app/src/main/res/drawable/baseline_privacy_tip_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values-night-v31/themes.xml b/app/src/main/res/values-night-v31/themes.xml index 69fecb4..c5a8f70 100644 --- a/app/src/main/res/values-night-v31/themes.xml +++ b/app/src/main/res/values-night-v31/themes.xml @@ -5,4 +5,10 @@ \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index a3dfc7d..f19f2d2 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -5,4 +5,10 @@ \ No newline at end of file diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml index 185d779..7023535 100644 --- a/app/src/main/res/values-v31/themes.xml +++ b/app/src/main/res/values-v31/themes.xml @@ -70,6 +70,12 @@ + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 49db6d9..f303356 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -22,4 +22,21 @@ @string/theme_transparent_light @string/theme_light + + + @string/update_frequency_15 + @string/update_frequency_30 + @string/update_frequency_60 + @string/update_frequency_360 + @string/update_frequency_720 + @string/update_frequency_1440 + + + 15 + 30 + 60 + 360 + 720 + 1440 + \ 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 5ea2be2..ad3fbb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -392,4 +392,13 @@ %s is available from a online repo. We strongly advise you to clean install it because most modules do not handle re-installation gracefully, but you can reinstall instead at your own risk. AMM debug build %1$s built on %2$s with %3$d days remaining %s is available from a online repo. Please uninstall it to see it in the online tab. + Credits + Background check frequency + How often to check for updates in the background. Setting too low a value could result in battery drain. + 15 minutes + 30 minutes + Hourly + 6 hours + 12 hours + Daily diff --git a/app/src/main/res/xml/app_info_preferences.xml b/app/src/main/res/xml/app_info_preferences.xml new file mode 100644 index 0000000..ad102fa --- /dev/null +++ b/app/src/main/res/xml/app_info_preferences.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/credits_preferences.xml b/app/src/main/res/xml/credits_preferences.xml new file mode 100644 index 0000000..4580d66 --- /dev/null +++ b/app/src/main/res/xml/credits_preferences.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/debugging_preferences.xml b/app/src/main/res/xml/debugging_preferences.xml new file mode 100644 index 0000000..7faaf23 --- /dev/null +++ b/app/src/main/res/xml/debugging_preferences.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/privacy_preferences.xml b/app/src/main/res/xml/privacy_preferences.xml new file mode 100644 index 0000000..9c44913 --- /dev/null +++ b/app/src/main/res/xml/privacy_preferences.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 48d9370..677206a 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -5,317 +5,83 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/security_preferences.xml b/app/src/main/res/xml/security_preferences.xml new file mode 100644 index 0000000..a34a511 --- /dev/null +++ b/app/src/main/res/xml/security_preferences.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/theme_preferences.xml b/app/src/main/res/xml/theme_preferences.xml new file mode 100644 index 0000000..2a4caf9 --- /dev/null +++ b/app/src/main/res/xml/theme_preferences.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/update_preferences.xml b/app/src/main/res/xml/update_preferences.xml new file mode 100644 index 0000000..7876032 --- /dev/null +++ b/app/src/main/res/xml/update_preferences.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8a6afae..971f997 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:8.0.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22") - classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:10.8.0") + classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:10.8.3") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties index 8f70f13..5456291 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,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 Jun 26 15:44:05 EDT 2023 +#Mon Jul 17 21:53:32 EDT 2023 android.defaults.buildfeatures.buildconfig=true android.enableJetifier=false android.enableR8.fullMode=true @@ -18,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 -Dorg.gradle.android.cache-fix.ignoreVersionCheck=true -Dkotlin.daemon.jvm.options\="-Xmx1536M" -Dfile.encoding\=UTF-8 -XX\:+UseParallelGC -XX\:ReservedCodeCacheSize\=768m +org.gradle.jvmargs=-Xmx1024M -Dorg.gradle.android.cache-fix.ignoreVersionCheck\=true -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Dfile.encoding\=UTF-8 -XX\:+UseParallelGC -XX\:ReservedCodeCacheSize\=768m org.gradle.parallel=true