You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
350 lines
20 KiB
Kotlin
350 lines
20 KiB
Kotlin
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.appcompat.app.AppCompatActivity
|
|
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.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
|
|
import androidx.core.content.edit
|
|
|
|
class UpdateFragment : PreferenceFragmentCompat() {
|
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
|
|
val name = "mmmx"
|
|
val context: Context? = MainApplication.getInstance()
|
|
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)
|
|
// 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.IS_WRAPPED && 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)
|
|
if (MainApplication.forceDebugLogging) 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.IS_WRAPPED
|
|
// 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.getPreferences("mmm")!!.edit {
|
|
putBoolean("pref_background_update_check", false)
|
|
}
|
|
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.IS_WRAPPED
|
|
backgroundUpdateCheck.onPreferenceChangeListener =
|
|
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
|
val enabled = java.lang.Boolean.parseBoolean(newValue.toString())
|
|
debugNotification.isEnabled = enabled
|
|
debugNotification.isVisible =
|
|
MainApplication.isDeveloper && !MainApplication.IS_WRAPPED && enabled
|
|
updateCheckExcludes.isEnabled = enabled
|
|
updateCheckExcludes.isVisible = enabled && !MainApplication.IS_WRAPPED
|
|
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)
|
|
if (MainApplication.forceDebugLogging) 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
|
|
)
|
|
}
|
|
}.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.IS_WRAPPED
|
|
updateCheckVersionExcludes.onPreferenceClickListener =
|
|
Preference.OnPreferenceClickListener {
|
|
// get the stringset pref_background_update_check_excludes_version
|
|
val stringSet = sharedPreferences.getStringSet(
|
|
"pref_background_update_check_excludes_version", HashSet()
|
|
)
|
|
if (MainApplication.forceDebugLogging) 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 ->
|
|
if (MainApplication.forceDebugLogging) 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
|
|
)
|
|
if (MainApplication.forceDebugLogging) Timber.d("text is %s for %s", text, editText.hint.toString())
|
|
} else {
|
|
if (MainApplication.forceDebugLogging) Timber.d("text is empty for %s", editText.hint.toString())
|
|
}
|
|
}
|
|
sharedPreferences.edit {
|
|
putStringSet(
|
|
"pref_background_update_check_excludes_version", stringSetTemp
|
|
)
|
|
}
|
|
}.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
|
|
.show()
|
|
}
|
|
true
|
|
}
|
|
|
|
val clipboard =
|
|
requireContext().getSystemService(AppCompatActivity.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.IS_WRAPPED
|
|
debugDownload.onPreferenceClickListener =
|
|
Preference.OnPreferenceClickListener { _: Preference? ->
|
|
val intent = Intent(requireContext(), UpdateActivity::class.java)
|
|
intent.action = UpdateActivity.ACTIONS.DOWNLOAD.name
|
|
startActivity(intent)
|
|
true
|
|
}
|
|
}
|
|
}
|