update settings

- new fragment based categories
- allow to control update check interval

Known issue:
- licenses screen is now an activity but may not respect user theme if it doesn't match system

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/89/head
androidacy-user 3 years ago
parent b429b7ca07
commit 5fc8059501

@ -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")

@ -162,7 +162,9 @@
</activity>
<activity
android:name="com.mikepenz.aboutlibraries.ui.LibsActivity"
tools:node="remove" />
android:exported="false"
android:parentActivityName=".settings.SettingsActivity"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
<provider
android:name="androidx.startup.InitializationProvider"

@ -420,13 +420,16 @@ class BackgroundUpdateChecker(context: Context, workerParams: WorkerParameters)
).build()
)
notificationManagerCompat.cancel(NOTIFICATION_ID_ONGOING)
// schedule periodic check for updates every 6 hours (6 * 60 * 60 = 21600) and not on low battery
Timber.d("Scheduling periodic background check")
// use pref_background_update_check_frequency to set frequency. value is in minutes
val frequency = MainApplication.getSharedPreferences("mmm")!!
.getInt("pref_background_update_check_frequency", 60).toLong()
Timber.d("Frequency: $frequency minutes")
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"background_checker",
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder(
BackgroundUpdateChecker::class.java, 21600, TimeUnit.SECONDS
BackgroundUpdateChecker::class.java, frequency, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder().setRequiresBatteryNotLow(true).build()
).build()

@ -0,0 +1,222 @@
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.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.preference.ListPreference
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.MainApplication
import com.fox2code.mmm.R
import com.fox2code.rosettax.LanguageSwitcher
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.topjohnwu.superuser.internal.UiThreadHandler
import timber.log.Timber
class AppearanceFragment : 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.theme_preferences, rootKey)
RepoFragment.applyMaterial3(preferenceScreen)
val themePreference = findPreference<ListPreference>("pref_theme")
// If transparent theme(s) are set, disable monet
if (themePreference!!.value == "transparent_light") {
Timber.d("disabling monet")
findPreference<Preference>("pref_enable_monet")!!.isEnabled = false
// Toggle monet off
(findPreference<Preference>("pref_enable_monet") as TwoStatePreference?)!!.isChecked =
false
editor.putBoolean("pref_enable_monet", false).apply()
// Set summary
findPreference<Preference>("pref_enable_monet")!!.setSummary(R.string.monet_disabled_summary)
// Same for blur
findPreference<Preference>("pref_enable_blur")!!.isEnabled = false
(findPreference<Preference>("pref_enable_blur") as TwoStatePreference?)!!.isChecked =
false
editor.putBoolean("pref_enable_blur", false).apply()
findPreference<Preference>("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<Preference>("pref_enable_monet") as TwoStatePreference?)!!.isChecked =
false
editor.putBoolean("pref_enable_monet", false).apply()
// Set summary
findPreference<Preference>("pref_enable_monet")!!.setSummary(R.string.monet_disabled_summary)
// Same for blur
(findPreference<Preference>("pref_enable_blur") as TwoStatePreference?)!!.isChecked =
false
editor.putBoolean("pref_enable_blur", false).apply()
findPreference<Preference>("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<Preference>("pref_theme") as ListPreference?)!!.value =
"system"
// Refresh activity
}.show()
} else {
findPreference<Preference>("pref_enable_monet")!!.isEnabled = true
findPreference<Preference>("pref_enable_monet")?.summary = ""
findPreference<Preference>("pref_enable_blur")!!.isEnabled = true
findPreference<Preference>("pref_enable_blur")?.summary = ""
}
UiThreadHandler.handler.postDelayed({
MainApplication.INSTANCE!!.updateTheme()
FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.INSTANCE!!.getManagerThemeResId())
}, 1)
true
}
val disableMonet = findPreference<Preference>("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<Preference>("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<Preference>("pref_enable_blur") as TwoStatePreference?)!!.isChecked =
true
editor.putBoolean("pref_enable_blur", true).apply()
// Set summary
findPreference<Preference>("pref_enable_blur")!!.setSummary(R.string.blur_disabled_summary)
}
.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
// Revert to blur on
(findPreference<Preference>("pref_enable_blur") as TwoStatePreference?)!!.isChecked =
false
editor.putBoolean("pref_enable_blur", false).apply()
// Set summary
findPreference<Preference>("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<LongClickablePreference>("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<Preference>("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
}
}
}

@ -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
}
}
}

@ -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<Preference>("pref_disable_low_quality_module_filter")!!.isVisible = false
// Find pref_clear_data and set it invisible
findPreference<Preference>("pref_clear_data")!!.isVisible = false
}
// hande clear cache
findPreference<Preference>("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<Preference?>("pref_test_crash")!!.isVisible = false
} else {
if (findPreference<Preference?>("pref_test_crash") != null && findPreference<Preference?>(
"pref_clear_data"
) != null
) {
findPreference<Preference>("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<Preference>("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<Preference>("pref_use_magisk_install_command")!!.isVisible = false
}
// handle pref_save_logs which saves logs to our external storage and shares them
val saveLogs = findPreference<Preference>("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<String> = 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
}
}
}

@ -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<Preference>("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<LongClickablePreference>("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<LongClickablePreference>("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
}
}
}

@ -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<TwoStatePreference>("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
}
}
}

@ -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<Preference>("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<Preference>("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<SwitchPreferenceCompat>("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<LongClickablePreference>("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<LongClickablePreference>("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<EditTextPreference>("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<String>()
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<Preference>("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<Preference>("pref_custom_repo_$i")
?: continue
preference.isVisible = false
}
} else {
for (i in 0..4) {
val preference = findPreference<Preference>("pref_custom_repo_$i")
?: continue
if (i >= custRepoEntries) {
preference.isVisible = false
}
}
}
var preference = findPreference<Preference>("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<Preference>(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<Preference>(preferenceName + "_website")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
IntentHelper.openUrl(
FoxActivity.getFoxActivity(this),
repoData.getWebsite()
)
true
}
} else {
findPreference<Preference>(preferenceName + "_website")!!.isVisible =
false
}
if (repoData.getSupport() != null) {
findPreference<Preference>(preferenceName + "_support")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
IntentHelper.openUrl(
FoxActivity.getFoxActivity(this),
repoData.getSupport()
)
true
}
} else {
findPreference<Preference>("${preferenceName}_support")!!.isVisible =
false
}
if (repoData.getSubmitModule() != null) {
findPreference<Preference>(preferenceName + "_submit")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
IntentHelper.openUrl(
FoxActivity.getFoxActivity(this),
repoData.getSubmitModule()
)
true
}
} else {
findPreference<Preference>(preferenceName + "_submit")!!.isVisible =
false
}
if (repoData.getDonate() != null) {
findPreference<Preference>(preferenceName + "_donate")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
IntentHelper.openUrl(
FoxActivity.getFoxActivity(this),
repoData.getDonate()
)
true
}
} else {
findPreference<Preference>(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<Preference>(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
}
}
}

@ -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<Preference>("pref_dns_over_https")!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, v: Any? ->
Http.setDoh(
(v as Boolean?)!!
)
true
}
// handle restart required for showcase mode
findPreference<Preference>("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<Preference>("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<Preference>("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
}
}
}

@ -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<Preference>("pref_background_update_check_debug")
val updateCheckExcludes =
findPreference<Preference>("pref_background_update_check_excludes")
val updateCheckVersionExcludes =
findPreference<Preference>("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<String, String>()
// 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<Preference>("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<LocalModuleInfo?> =
ModuleManager.instance!!.modules.values
// make sure we have modules
val checkedItems: BooleanArray
if (!localModuleInfos.isEmpty()) {
val moduleNames = arrayOfNulls<String>(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<String> = 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<LocalModuleInfo?> =
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<String> = 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<LongClickablePreference>("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<Preference>("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
}
}
}

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10C17.5,22 22,17.5 22,12S17.5,2 12,2zM12,20c-4.4,0 -8,-3.6 -8,-8s3.6,-8 8,-8 8,3.6 8,8 -3.6,8 -8,8z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.5,7H11v6l5.3,3.2 0.8,-1.2 -4.5,-2.7z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16,11c1.7,0 3,-1.3 3,-3S17.7,5 16,5c-1.7,0 -3,1.3 -3,3s1.3,3 3,3zM8,11c1.7,0 3,-1.3 3,-3S9.7,5 8,5C6.3,5 5,6.3 5,8s1.3,3 3,3zM8,13c-2.3,0 -7,1.2 -7,3.5L1,19h14v-2.5c0,-2.3 -4.7,-3.5 -7,-3.5zM16,13c-0.3,0 -0.6,0 -1,0.1 1.2,0.8 2,2 2,3.5L17,19h6v-2.5c0,-2.3 -4.7,-3.5 -7,-3.5z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.6 3.8,10.7 9,12c5.2,-1.3 9,-6.5 9,-12V5L12,1L12,1zM11,7h2v2h-2V7zM11,11h2v6h-2V11z"/>
</vector>

@ -5,4 +5,10 @@
<resources>
<style name="Theme.MagiskModuleManager.Monet" parent="Theme.MagiskModuleManager.Monet.Dark" />
<style name="Theme.MagiskModuleManager.NoActionBar" parent="Theme.MagiskModuleManager.Monet">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

@ -5,4 +5,10 @@
<resources>
<style name="Theme.MagiskModuleManager" parent="Theme.MagiskModuleManager.Dark" />
<style name="Theme.MagiskModuleManager.NoActionBar" parent="Theme.MagiskModuleManager">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

@ -70,6 +70,12 @@
</style>
<!-- I have no idea why this is needed, but it is -->
<style name="Theme.MagiskModuleManager.NoActionBar" parent="Theme.MagiskModuleManager.Monet">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<!-- Base application theme. -->

@ -22,4 +22,21 @@
<item>@string/theme_transparent_light</item>
<item>@string/theme_light</item>
</string-array>
<string-array name="notification_update_frequency_entries" translatable="false">
<!-- 15, 30 min then 1 hour, six hours, 12 hours, daily -->
<item>@string/update_frequency_15</item>
<item>@string/update_frequency_30</item>
<item>@string/update_frequency_60</item>
<item>@string/update_frequency_360</item>
<item>@string/update_frequency_720</item>
<item>@string/update_frequency_1440</item>
</string-array>
<string-array name="notification_update_frequency_values" translatable="false">
<item>15</item>
<item>30</item>
<item>60</item>
<item>360</item>
<item>720</item>
<item>1440</item>
</string-array>
</resources>

@ -392,4 +392,13 @@
<string name="remote_message">%s is available from a online repo. <b>We strongly advise you to clean install it because most modules do not handle re-installation gracefully,</b> but you can reinstall instead at your own risk.</string>
<string name="debug_build_toast">AMM debug build %1$s built on %2$s with %3$d days remaining</string>
<string name="remote_message_no_update">%s is available from a online repo. Please uninstall it to see it in the online tab.</string>
<string name="credits">Credits</string>
<string name="notification_update_frequency_pref">Background check frequency</string>
<string name="notification_update_frequency_desc">How often to check for updates in the background. Setting too low a value could result in battery drain.</string>
<string name="update_frequency_15">15 minutes</string>
<string name="update_frequency_30">30 minutes</string>
<string name="update_frequency_60">Hourly</string>
<string name="update_frequency_360">6 hours</string>
<string name="update_frequency_720">12 hours</string>
<string name="update_frequency_1440">Daily</string>
</resources>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_info">
<!-- donate buttons for fox2code and androidacy -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_monetization_on_24"
app:key="pref_donate_fox"
app:singleLineTitle="false"
app:title="@string/donate_fox" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_monetization_on_24"
app:key="pref_donate_androidacy"
app:singleLineTitle="false"
app:summary="@string/donate_androidacy_sum"
app:title="@string/donate_androidacy" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_report_bug"
app:singleLineTitle="false"
app:title="@string/report_bugs" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_github"
app:key="pref_source_code"
app:singleLineTitle="false"
app:title="@string/source_code" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_telegram_24"
app:key="pref_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_telegram_24"
app:key="pref_announcements"
app:singleLineTitle="false"
app:title="@string/announcements" />
</PreferenceCategory>
</PreferenceScreen>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_contributors">
<!-- Small lil thanks to Androidacy -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/baseline_favorite_24"
app:key="pref_androidacy_thanks"
app:singleLineTitle="false"
app:summary="@string/androidacy_thanks_desc"
app:title="@string/androidacy_thanks" />
<!-- Small lil thanks to Fox2Code -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/baseline_favorite_24"
app:key="pref_fox2code_thanks"
app:singleLineTitle="false"
app:summary="@string/fox2code_thanks_desc"
app:title="@string/fox2code_thanks" />
<!-- OKay, so we'll thank all the other contributors too -->
<com.fox2code.mmm.settings.LongClickablePreference
android:textAppearance="?android:attr/textAppearanceSmall"
app:iconSpaceReserved="false"
app:key="pref_contributors"
app:singleLineTitle="false"
app:title="@string/contributors" />
<!-- And the translators -->
</PreferenceCategory>
</PreferenceScreen>

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/debug_cat">
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_hide_source_24"
app:key="pref_show_incompatible"
app:singleLineTitle="false"
app:summary="@string/show_incompatible_desc"
app:title="@string/show_incompatible_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_warning_24"
app:key="pref_disable_low_quality_module_filter"
app:singleLineTitle="false"
app:summary="@string/disable_low_quality_module_filter_desc"
app:title="@string/disable_low_quality_module_filter_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_numbers_24"
app:key="pref_use_magisk_install_command"
app:singleLineTitle="false"
app:summary="@string/use_magisk_install_command_desc"
app:title="@string/use_magisk_install_command_pref" />
<!-- Purposely crash the app -->
<Preference
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_test_crash"
app:singleLineTitle="false"
app:title="@string/crash" />
<!-- Pref to clear the app data -->
<Preference
app:icon="@drawable/ic_baseline_delete_24"
app:key="pref_clear_data"
app:singleLineTitle="false"
app:title="@string/clear_app_data" />
<!-- clear app cache -->
<Preference
app:icon="@drawable/ic_baseline_delete_24"
app:key="pref_clear_cache"
app:singleLineTitle="false"
app:summary="@string/clear_app_cache_desc"
app:title="@string/clear_app_cache" />
<!-- Save logs -->
<Preference
app:icon="@drawable/baseline_save_24"
app:key="pref_save_logs"
app:singleLineTitle="false"
app:title="@string/save_logs" />
</PreferenceCategory>
</PreferenceScreen>

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_privacy">
<!-- Crash reporting -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_crash_reporting"
app:singleLineTitle="false"
app:summary="@string/crash_reporting_desc"
app:title="@string/crash_reporting" />
<!-- allow pii in crash reports -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:dependency="pref_crash_reporting"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_crash_reporting_pii"
app:singleLineTitle="false"
app:summary="@string/crash_reporting_pii_desc"
app:title="@string/crash_reporting_pii" />
<!-- analytics enabled -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_analytics_enabled"
app:singleLineTitle="false"
app:summary="@string/analytics_desc"
app:title="@string/setup_app_analytics" />
</PreferenceCategory>
</PreferenceScreen>

@ -5,317 +5,83 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:icon="@drawable/ic_baseline_info_24"
<Preference
app:icon="@drawable/ic_baseline_warning_24"
android:enabled="false"
app:title="@string/warning_pls_restart" />
<!-- Custom repos has been announced, check https://github.com/Androidacy/MagiskModuleManager/issues/131 -->
<PreferenceCategory app:title="@string/pref_category_repos">
<Preference
app:icon="@drawable/ic_baseline_extension_24"
app:key="pref_manage_repos"
app:singleLineTitle="false"
app:title="@string/manage_repos_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_hide_source_24"
app:key="pref_show_incompatible"
app:singleLineTitle="false"
app:summary="@string/show_incompatible_desc"
app:title="@string/show_incompatible_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_warning_24"
app:key="pref_disable_low_quality_module_filter"
app:singleLineTitle="false"
app:summary="@string/disable_low_quality_module_filter_desc"
app:title="@string/disable_low_quality_module_filter_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_numbers_24"
app:key="pref_use_magisk_install_command"
app:singleLineTitle="false"
app:summary="@string/use_magisk_install_command_desc"
app:title="@string/use_magisk_install_command_pref" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_updates">
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_notifications_24"
app:key="pref_background_update_check"
app:singleLineTitle="false"
app:summary="@string/notification_update_desc"
app:title="@string/notification_update_pref" />
<!-- check for app updates -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_app_settings_alt_24"
app:key="pref_background_update_check_app"
app:singleLineTitle="false"
app:summary="@string/notification_update_app_desc"
app:title="@string/notification_update_app_pref" />
<!-- require wifi -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:dependency="pref_background_update_check"
app:icon="@drawable/baseline_network_wifi_24"
app:key="pref_background_update_check_wifi"
app:singleLineTitle="false"
app:summary="@string/notification_update_wifi_pref"
app:title="@string/notification_update_wifi_desc" />
<!-- Ignore updates for preference. Used to ignore updates for specific modules -->
<Preference
app:icon="@drawable/baseline_block_24"
app:key="pref_background_update_check_excludes"
app:singleLineTitle="false"
app:summary="@string/notification_update_ignore_desc"
app:title="@string/notification_update_ignore_pref" />
<!-- exclude specific versions -->
<Preference
app:icon="@drawable/baseline_block_24"
app:key="pref_background_update_check_excludes_version"
app:singleLineTitle="false"
app:summary="@string/notification_update_ignore_version_desc"
app:title="@string/notification_update_ignore_version_pref" />
<Preference
app:icon="@drawable/baseline_notification_important_24"
app:key="pref_background_update_check_debug"
app:singleLineTitle="false"
app:title="@string/notification_update_debug_pref" />
<!-- For debugging: launch update activity with download action -->
<Preference
app:icon="@drawable/ic_baseline_download_24"
app:key="pref_background_update_check_debug_download"
app:singleLineTitle="false"
app:title="@string/update_debug_download_pref" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_system_update_24"
app:key="pref_update"
app:singleLineTitle="false"
app:title="@string/check_for_updates" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_appearance">
<ListPreference
app:defaultValue="system"
app:entries="@array/theme_values_names"
app:entryValues="@array/theme_values"
app:icon="@drawable/ic_baseline_palette_24"
app:key="pref_theme"
app:singleLineTitle="false"
app:title="@string/theme_pref" />
<Preference
app:icon="@drawable/ic_baseline_language_24"
app:key="pref_language_selector"
app:title="@string/language" />
<!-- Call to action for translators -->
<com.fox2code.mmm.settings.LongClickablePreference
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="8sp"
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_language_selector_cta"
app:singleLineTitle="false"
app:summary="@string/language_cta_desc"
app:title="@string/language_cta" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_blur_on_24"
app:key="pref_enable_blur"
app:singleLineTitle="false"
app:summary="@string/blur_desc"
app:title="@string/enable_blur_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_list_24"
app:key="pref_force_dark_terminal"
app:singleLineTitle="false"
app:title="@string/force_dark_terminal_title" />
<SwitchPreferenceCompat
app:defaultValue="@bool/monet_enabled_by_default"
app:icon="@drawable/ic_baseline_design_services_24"
app:key="pref_enable_monet"
app:singleLineTitle="false"
app:title="@string/enable_monet" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_keyboard_return_24"
app:key="pref_wrap_text"
app:singleLineTitle="false"
app:summary="@string/wrap_text_desc"
app:title="@string/wrap_text_pref" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_security">
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_security_24"
app:key="pref_dns_over_https"
app:singleLineTitle="false"
app:summary="@string/dns_over_https_desc"
app:title="@string/dns_over_https_pref" />
<!-- TO DO: figure out why the f*** we need a showcase mode -->
<!-- like seriously, why? -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_lock_24"
app:key="pref_showcase_mode"
app:singleLineTitle="false"
app:summary="@string/showcase_mode_desc"
app:title="@string/showcase_mode_pref" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_reboot_24"
app:key="pref_prevent_reboot"
app:singleLineTitle="false"
app:summary="@string/prevent_reboot_desc"
app:title="@string/prevent_reboot_pref" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_privacy">
<!-- Crash reporting -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_crash_reporting"
app:singleLineTitle="false"
app:summary="@string/crash_reporting_desc"
app:title="@string/crash_reporting" />
<!-- allow pii in crash reports -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:dependency="pref_crash_reporting"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_crash_reporting_pii"
app:singleLineTitle="false"
app:summary="@string/crash_reporting_pii_desc"
app:title="@string/crash_reporting_pii" />
<!-- analytics enabled -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_analytics_enabled"
app:singleLineTitle="false"
app:summary="@string/analytics_desc"
app:title="@string/setup_app_analytics" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/debug_cat">
<!-- Purposely crash the app -->
<Preference
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_test_crash"
app:singleLineTitle="false"
app:title="@string/crash" />
<!-- Pref to clear the app data -->
<Preference
app:icon="@drawable/ic_baseline_delete_24"
app:key="pref_clear_data"
app:singleLineTitle="false"
app:title="@string/clear_app_data" />
<!-- clear app cache -->
<Preference
app:icon="@drawable/ic_baseline_delete_24"
app:key="pref_clear_cache"
app:singleLineTitle="false"
app:summary="@string/clear_app_cache_desc"
app:title="@string/clear_app_cache" />
<!-- Save logs -->
<Preference
app:icon="@drawable/baseline_save_24"
app:key="pref_save_logs"
app:singleLineTitle="false"
app:title="@string/save_logs" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_info">
<!-- donate buttons for fox2code and androidacy -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_monetization_on_24"
app:key="pref_donate_fox"
app:singleLineTitle="false"
app:title="@string/donate_fox" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_monetization_on_24"
app:key="pref_donate_androidacy"
app:singleLineTitle="false"
app:summary="@string/donate_androidacy_sum"
app:title="@string/donate_androidacy" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_report_bug"
app:singleLineTitle="false"
app:title="@string/report_bugs" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_github"
app:key="pref_source_code"
app:singleLineTitle="false"
app:title="@string/source_code" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_telegram_24"
app:key="pref_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_telegram_24"
app:key="pref_announcements"
app:singleLineTitle="false"
app:title="@string/announcements" />
<Preference
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_show_licenses"
app:singleLineTitle="false"
app:title="@string/show_licenses" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_contributors">
<!-- Small lil thanks to Androidacy -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/baseline_favorite_24"
app:key="pref_androidacy_thanks"
app:singleLineTitle="false"
app:summary="@string/androidacy_thanks_desc"
app:title="@string/androidacy_thanks" />
<!-- Small lil thanks to Fox2Code -->
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/baseline_favorite_24"
app:key="pref_fox2code_thanks"
app:singleLineTitle="false"
app:summary="@string/fox2code_thanks_desc"
app:title="@string/fox2code_thanks" />
<!-- OKay, so we'll thank all the other contributors too -->
<com.fox2code.mmm.settings.LongClickablePreference
android:textAppearance="?android:attr/textAppearanceSmall"
app:iconSpaceReserved="false"
app:key="pref_contributors"
app:singleLineTitle="false"
app:title="@string/contributors" />
<!-- And the translators -->
<Preference
app:enabled="true"
app:iconSpaceReserved="false"
app:key="pref_pkg_info"
app:singleLineTitle="false"
app:summary="@string/loading" />
</PreferenceCategory>
<Preference
android:fragment="com.fox2code.mmm.settings.RepoFragment"
app:icon="@drawable/ic_baseline_extension_24"
app:key="pref_manage_repos"
app:singleLineTitle="false"
app:title="@string/manage_repos_pref" />
<!-- updates, appearance, security, privacy, info, debugging, credits, etc. -->
<Preference
android:fragment="com.fox2code.mmm.settings.UpdateFragment"
app:icon="@drawable/ic_baseline_update_24"
app:key="pref_updates_screen"
app:singleLineTitle="false"
app:title="@string/pref_category_updates" />
<Preference
android:fragment="com.fox2code.mmm.settings.AppearanceFragment"
app:icon="@drawable/ic_baseline_palette_24"
app:key="pref_appearance"
app:singleLineTitle="false"
app:title="@string/pref_category_appearance" />
<Preference
android:fragment="com.fox2code.mmm.settings.SecurityFragment"
app:icon="@drawable/ic_baseline_security_24"
app:key="pref_security"
app:singleLineTitle="false"
app:title="@string/pref_category_security" />
<Preference
android:fragment="com.fox2code.mmm.settings.PrivacyFragment"
app:icon="@drawable/baseline_privacy_tip_24"
app:key="pref_privacy"
app:singleLineTitle="false"
app:title="@string/pref_category_privacy" />
<Preference
android:fragment="com.fox2code.mmm.settings.DebugFragment"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_debug"
app:singleLineTitle="false"
app:title="@string/debug_cat" />
<Preference
android:fragment="com.fox2code.mmm.settings.InfoFragment"
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_info"
app:singleLineTitle="false"
app:title="@string/pref_category_info" />
<Preference
android:fragment="com.fox2code.mmm.settings.CreditsFragment"
app:icon="@drawable/baseline_people_24"
app:key="pref_credits"
app:singleLineTitle="false"
app:title="@string/credits" />
<Preference
app:icon="@drawable/ic_baseline_list_24"
app:key="pref_show_licenses"
app:singleLineTitle="false"
app:title="@string/show_licenses" />
<Preference
app:enabled="true"
app:iconSpaceReserved="false"
app:key="pref_pkg_info"
app:singleLineTitle="false"
app:summary="@string/loading" />
</PreferenceScreen>

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_security">
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_security_24"
app:key="pref_dns_over_https"
app:singleLineTitle="false"
app:summary="@string/dns_over_https_desc"
app:title="@string/dns_over_https_pref" />
<!-- TO DO: figure out why the f*** we need a showcase mode -->
<!-- like seriously, why? -->
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_lock_24"
app:key="pref_showcase_mode"
app:singleLineTitle="false"
app:summary="@string/showcase_mode_desc"
app:title="@string/showcase_mode_pref" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_reboot_24"
app:key="pref_prevent_reboot"
app:singleLineTitle="false"
app:summary="@string/prevent_reboot_desc"
app:title="@string/prevent_reboot_pref" />
</PreferenceCategory>
</PreferenceScreen>

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_appearance">
<ListPreference
app:defaultValue="system"
app:entries="@array/theme_values_names"
app:entryValues="@array/theme_values"
app:icon="@drawable/ic_baseline_palette_24"
app:key="pref_theme"
app:singleLineTitle="false"
app:title="@string/theme_pref" />
<Preference
app:icon="@drawable/ic_baseline_language_24"
app:key="pref_language_selector"
app:title="@string/language" />
<!-- Call to action for translators -->
<com.fox2code.mmm.settings.LongClickablePreference
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="8sp"
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_language_selector_cta"
app:singleLineTitle="false"
app:summary="@string/language_cta_desc"
app:title="@string/language_cta" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_blur_on_24"
app:key="pref_enable_blur"
app:singleLineTitle="false"
app:summary="@string/blur_desc"
app:title="@string/enable_blur_pref" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_list_24"
app:key="pref_force_dark_terminal"
app:singleLineTitle="false"
app:title="@string/force_dark_terminal_title" />
<SwitchPreferenceCompat
app:defaultValue="@bool/monet_enabled_by_default"
app:icon="@drawable/ic_baseline_design_services_24"
app:key="pref_enable_monet"
app:singleLineTitle="false"
app:title="@string/enable_monet" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:icon="@drawable/ic_baseline_keyboard_return_24"
app:key="pref_wrap_text"
app:singleLineTitle="false"
app:summary="@string/wrap_text_desc"
app:title="@string/wrap_text_pref" />
</PreferenceCategory>
</PreferenceScreen>

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_category_updates">
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_notifications_24"
app:key="pref_background_update_check"
app:singleLineTitle="false"
app:summary="@string/notification_update_desc"
app:title="@string/notification_update_pref" />
<!-- check for app updates -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_app_settings_alt_24"
app:key="pref_background_update_check_app"
app:singleLineTitle="false"
app:summary="@string/notification_update_app_desc"
app:title="@string/notification_update_app_pref" />
<!-- require wifi -->
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/baseline_network_wifi_24"
app:key="pref_background_update_check_wifi"
app:singleLineTitle="false"
app:summary="@string/notification_update_wifi_pref"
app:title="@string/notification_update_wifi_desc" />
<!-- update check frequency -->
<ListPreference
app:defaultValue="0"
app:entries="@array/notification_update_frequency_entries"
app:entryValues="@array/notification_update_frequency_values"
app:icon="@drawable/baseline_access_time_24"
app:key="pref_background_update_check_frequency"
app:singleLineTitle="false"
android:defaultValue="360"
app:summary="@string/notification_update_frequency_desc"
app:title="@string/notification_update_frequency_pref" />
<!-- Ignore updates for preference. Used to ignore updates for specific modules -->
<Preference
app:icon="@drawable/baseline_block_24"
android:dependency="pref_background_update_check"
app:key="pref_background_update_check_excludes"
app:singleLineTitle="false"
app:summary="@string/notification_update_ignore_desc"
app:title="@string/notification_update_ignore_pref" />
<!-- exclude specific versions -->
<Preference
app:icon="@drawable/baseline_block_24"
android:dependency="pref_background_update_check"
app:key="pref_background_update_check_excludes_version"
app:singleLineTitle="false"
app:summary="@string/notification_update_ignore_version_desc"
app:title="@string/notification_update_ignore_version_pref" />
<Preference
app:icon="@drawable/baseline_notification_important_24"
app:key="pref_background_update_check_debug"
app:singleLineTitle="false"
app:title="@string/notification_update_debug_pref" />
<!-- For debugging: launch update activity with download action -->
<Preference
app:icon="@drawable/ic_baseline_download_24"
app:key="pref_background_update_check_debug_download"
app:singleLineTitle="false"
app:title="@string/update_debug_download_pref" />
<com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_system_update_24"
app:key="pref_update"
app:singleLineTitle="false"
app:title="@string/check_for_updates" />
</PreferenceCategory>
</PreferenceScreen>

@ -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

@ -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

Loading…
Cancel
Save