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.
767 lines
30 KiB
Kotlin
767 lines
30 KiB
Kotlin
/*
|
|
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
|
|
*/
|
|
|
|
package com.fox2code.mmm
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.app.Activity
|
|
import android.app.ActivityManager
|
|
import android.app.ActivityManager.RunningAppProcessInfo
|
|
import android.app.Application
|
|
import android.app.Application.ActivityLifecycleCallbacks
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.SharedPreferences
|
|
import android.content.pm.PackageManager
|
|
import android.content.res.Resources
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.SystemClock
|
|
import android.util.Log
|
|
import androidx.annotation.StyleRes
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.appcompat.view.ContextThemeWrapper
|
|
import androidx.core.app.NotificationManagerCompat
|
|
import androidx.emoji2.text.DefaultEmojiCompatConfig
|
|
import androidx.emoji2.text.EmojiCompat
|
|
import androidx.security.crypto.EncryptedSharedPreferences
|
|
import androidx.security.crypto.MasterKey
|
|
import androidx.work.Configuration
|
|
import cat.ereza.customactivityoncrash.config.CaocConfig
|
|
import com.fox2code.mmm.installer.InstallerInitializer
|
|
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion
|
|
import com.fox2code.mmm.manager.LocalModuleInfo
|
|
import com.fox2code.mmm.repo.RepoModule
|
|
import com.fox2code.mmm.utils.TimberUtils.configTimber
|
|
import com.fox2code.mmm.utils.io.FileUtils
|
|
import com.fox2code.mmm.utils.io.GMSProviderInstaller.Companion.installIfNeeded
|
|
import com.fox2code.mmm.utils.io.net.Http.Companion.getHttpClientWithCache
|
|
import com.fox2code.rosettax.LanguageSwitcher
|
|
import com.google.common.hash.Hashing
|
|
import com.topjohnwu.superuser.Shell
|
|
import io.noties.markwon.Markwon
|
|
import io.noties.markwon.html.HtmlPlugin
|
|
import io.noties.markwon.image.ImagesPlugin
|
|
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
|
import ly.count.android.sdk.Countly
|
|
import ly.count.android.sdk.CountlyConfig
|
|
import timber.log.Timber
|
|
import java.io.File
|
|
import java.security.SecureRandom
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Random
|
|
import kotlin.math.abs
|
|
|
|
|
|
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
|
class MainApplication : Application(), Configuration.Provider, ActivityLifecycleCallbacks {
|
|
|
|
private var callbacksRegistered = false
|
|
var isTainted = false
|
|
|
|
var lastActivity: AppCompatActivity? = null
|
|
|
|
var modulesHaveUpdates = false
|
|
|
|
var updateModuleCount = 0
|
|
|
|
var updateModules: List<String> = ArrayList()
|
|
|
|
@StyleRes
|
|
private var managerThemeResId = R.style.Theme_MagiskModuleManager
|
|
private var markwonThemeContext: ContextThemeWrapper? = null
|
|
|
|
var markwon: Markwon? = null
|
|
get() {
|
|
if (isCrashHandler) return null
|
|
if (field != null) return field
|
|
var contextThemeWrapper = markwonThemeContext
|
|
if (contextThemeWrapper == null) {
|
|
markwonThemeContext = ContextThemeWrapper(this, managerThemeResId)
|
|
contextThemeWrapper = markwonThemeContext
|
|
}
|
|
field = Markwon.builder(contextThemeWrapper!!).usePlugin(HtmlPlugin.create()).usePlugin(
|
|
ImagesPlugin.create().addSchemeHandler(
|
|
OkHttpNetworkSchemeHandler.create(
|
|
getHttpClientWithCache()!!
|
|
)
|
|
)
|
|
).build()
|
|
return field
|
|
}
|
|
private var existingKey: CharArray? = null
|
|
private var makingNewKey = false
|
|
private var isCrashHandler = false
|
|
|
|
var localModules: HashMap<String, LocalModuleInfo> = HashMap()
|
|
var repoModules: HashMap<String, RepoModule> = HashMap()
|
|
|
|
init {
|
|
check(!(INSTANCE != null && INSTANCE !== this)) { "Duplicate application instance!" }
|
|
INSTANCE = this
|
|
}
|
|
|
|
// generates or retrieves a key for encrypted room databases
|
|
@SuppressLint("ApplySharedPref")
|
|
fun getKey(): CharArray {
|
|
// check if existing key is available
|
|
if (existingKey != null) {
|
|
return existingKey!!
|
|
}
|
|
// use android keystore to generate a key
|
|
val masterKey = MasterKey.Builder(this).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
|
val sharedPreferences = EncryptedSharedPreferences.create(
|
|
this,
|
|
"dbKey",
|
|
masterKey,
|
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
)
|
|
val key = sharedPreferences.getString("dbKey", null)
|
|
if (key != null) {
|
|
existingKey = key.toCharArray()
|
|
return existingKey!!
|
|
}
|
|
// generate a new key
|
|
val newKey = CharArray(32)
|
|
val random = SecureRandom()
|
|
for (i in newKey.indices) {
|
|
newKey[i] = (random.nextInt(26) + 97).toChar()
|
|
}
|
|
sharedPreferences.edit().putString("dbKey", String(newKey)).commit()
|
|
existingKey = newKey
|
|
return existingKey!!
|
|
}
|
|
|
|
override fun getWorkManagerConfiguration(): Configuration {
|
|
return Configuration.Builder().build()
|
|
}
|
|
|
|
fun updateTheme() {
|
|
@StyleRes val themeResId: Int
|
|
var theme: String?
|
|
val monet = isMonetEnabled
|
|
when (getPreferences("mmm")!!.getString("pref_theme", "system").also { theme = it }) {
|
|
"system" -> themeResId =
|
|
if (monet) R.style.Theme_MagiskModuleManager_Monet else R.style.Theme_MagiskModuleManager
|
|
|
|
"dark" -> themeResId =
|
|
if (monet) R.style.Theme_MagiskModuleManager_Monet_Dark else R.style.Theme_MagiskModuleManager_Dark
|
|
|
|
"black" -> themeResId =
|
|
if (monet) R.style.Theme_MagiskModuleManager_Monet_Black else R.style.Theme_MagiskModuleManager_Black
|
|
|
|
"light" -> themeResId =
|
|
if (monet) R.style.Theme_MagiskModuleManager_Monet_Light else R.style.Theme_MagiskModuleManager_Light
|
|
|
|
"transparent_light" -> {
|
|
if (monet) {
|
|
Timber.tag("MainApplication").w("Monet is not supported for transparent theme")
|
|
}
|
|
themeResId = R.style.Theme_MagiskModuleManager_Transparent_Light
|
|
}
|
|
|
|
else -> {
|
|
Timber.w("Unknown theme id: %s", theme)
|
|
themeResId =
|
|
if (monet) R.style.Theme_MagiskModuleManager_Monet else R.style.Theme_MagiskModuleManager
|
|
}
|
|
}
|
|
setManagerThemeResId(themeResId)
|
|
}
|
|
|
|
@StyleRes
|
|
fun getManagerThemeResId(): Int {
|
|
return managerThemeResId
|
|
}
|
|
|
|
@SuppressLint("NonConstantResourceId")
|
|
fun setManagerThemeResId(@StyleRes resId: Int) {
|
|
managerThemeResId = resId
|
|
if (markwonThemeContext != null) {
|
|
markwonThemeContext!!.setTheme(resId)
|
|
}
|
|
markwon = null
|
|
}
|
|
|
|
val isLightTheme: Boolean
|
|
get() = when (managerThemeResId) {
|
|
R.style.Theme_MagiskModuleManager, R.style.Theme_MagiskModuleManager_Monet, R.style.Theme_MagiskModuleManager_Dark, R.style.Theme_MagiskModuleManager_Monet_Dark, R.style.Theme_MagiskModuleManager_Black, R.style.Theme_MagiskModuleManager_Monet_Black -> false
|
|
|
|
else -> true
|
|
}
|
|
|
|
private val isSystemLightTheme: Boolean
|
|
get() = (this.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) != android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
val isDarkTheme: Boolean
|
|
get() = !this.isLightTheme
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
CaocConfig.Builder.create()
|
|
.backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //default: CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM
|
|
.enabled(true) //default: true
|
|
.trackActivities(true) //default: false
|
|
.minTimeBetweenCrashesMs(2000)
|
|
.restartActivity(MainActivity::class.java) //default: null (your app's launch activity)
|
|
.errorActivity(CrashHandler::class.java) //default: null (default error activity)
|
|
.apply()
|
|
supportedLocales.addAll(
|
|
listOf(
|
|
"ar",
|
|
"bs",
|
|
"cs",
|
|
"de",
|
|
"es-rMX",
|
|
"es",
|
|
"el",
|
|
"fr",
|
|
"hu",
|
|
"id",
|
|
"it",
|
|
"ja",
|
|
"nl",
|
|
"pl",
|
|
"pt",
|
|
"pt-rBR",
|
|
"ru",
|
|
"tr",
|
|
"uk",
|
|
"vi",
|
|
"zh",
|
|
"zh-rTW",
|
|
"en"
|
|
)
|
|
)
|
|
if (INSTANCE == null) INSTANCE = this
|
|
relPackageName = this.packageName
|
|
var output = Shell.cmd("echo $(id -u)").exec().out[0]
|
|
if (output != null) {
|
|
output = output.trim { it <= ' ' }
|
|
if (forceDebugLogging) Timber.d("UID: %s", output)
|
|
if (output == "0") {
|
|
if (forceDebugLogging) Timber.d("Root access granted")
|
|
} else {
|
|
if (forceDebugLogging) Timber.d(
|
|
"Root access or we're not uid 0. Current uid: %s",
|
|
output
|
|
)
|
|
}
|
|
}
|
|
if (!callbacksRegistered) {
|
|
try {
|
|
registerActivityLifecycleCallbacks(this)
|
|
callbacksRegistered = true
|
|
} catch (e: Exception) {
|
|
Timber.e(e, "Failed to register activity lifecycle callbacks")
|
|
}
|
|
}
|
|
// Initialize Timber
|
|
configTimber()
|
|
if (forceDebugLogging) Timber.i(
|
|
"Starting AMM version %s (%d) - commit %s",
|
|
BuildConfig.VERSION_NAME,
|
|
BuildConfig.VERSION_CODE,
|
|
BuildConfig.COMMIT_HASH
|
|
)
|
|
// Update SSL Ciphers if update is possible
|
|
installIfNeeded(this)
|
|
// get intent. if isCrashing is not present or false, call FileUtils.ensureCacheDirs and FileUtils.ensureURLHandler
|
|
isCrashHandler = intent!!.getBooleanExtra("isCrashing", false)
|
|
if (!isCrashHandler) {
|
|
val fileUtils = FileUtils()
|
|
fileUtils.ensureCacheDirs()
|
|
fileUtils.ensureURLHandler(this)
|
|
}
|
|
if (forceDebugLogging) Timber.d("Initializing AMM")
|
|
if (forceDebugLogging) Timber.d("Started from background: %s", !isInForeground)
|
|
if (forceDebugLogging) Timber.d("AMM is running in debug mode")
|
|
// analytics
|
|
if (forceDebugLogging) Timber.d("Initializing countly")
|
|
if (!analyticsAllowed()) {
|
|
if (forceDebugLogging) Timber.d("countly is not allowed")
|
|
} else {
|
|
val config = CountlyConfig(
|
|
this,
|
|
"ff1dc022295f64a7a5f6a5ca07c0294400c71b60",
|
|
"https://ctly.androidacy.com"
|
|
)
|
|
if (isCrashReportingEnabled) {
|
|
config.enableCrashReporting()
|
|
}
|
|
config.enableAutomaticViewTracking()
|
|
config.setPushIntentAddMetadata(true)
|
|
config.setLoggingEnabled(BuildConfig.DEBUG)
|
|
config.setRequiresConsent(false)
|
|
config.setRecordAppStartTime(true)
|
|
Countly.sharedInstance().init(config)
|
|
Countly.applicationOnCreate()
|
|
}
|
|
try {
|
|
@Suppress("DEPRECATION") @SuppressLint("PackageManagerGetSignatures") val s =
|
|
this.packageManager.getPackageInfo(
|
|
this.packageName, PackageManager.GET_SIGNATURES
|
|
).signatures
|
|
val osh = arrayOf(
|
|
"7bec7c4462f4aac616612d9f56a023ee3046e83afa956463b5fab547fd0a0be6",
|
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
"e8ce7deca880304d7ff09f8fc37656cfa927cee7f6a0bb7b3feda6a5942931f5",
|
|
"339af2fb5b671fa4af6436b585351f2f1fc746d1d922f9a0b01df2d576381015"
|
|
)
|
|
val oosh = Hashing.sha256().hashBytes(s[0].toByteArray()).toString()
|
|
o = listOf(*osh).contains(oosh)
|
|
} catch (ignored: PackageManager.NameNotFoundException) {
|
|
}
|
|
val sharedPreferences = getPreferences("mmm")
|
|
// We are only one process so it's ok to do this
|
|
val bootPrefs = getPreferences("mmm_boot")
|
|
val lastBoot = System.currentTimeMillis() - SystemClock.elapsedRealtime()
|
|
val lastBootPrefs = bootPrefs!!.getLong("last_boot", 0)
|
|
isFirstBoot = if (lastBootPrefs == 0L || abs(lastBoot - lastBootPrefs) > 100) {
|
|
val firstBoot = sharedPreferences!!.getBoolean("first_boot", true)
|
|
bootPrefs.edit().clear().putLong("last_boot", lastBoot)
|
|
.putBoolean("first_boot", firstBoot).apply()
|
|
if (firstBoot) {
|
|
sharedPreferences.edit().putBoolean("first_boot", false).apply()
|
|
}
|
|
firstBoot
|
|
} else {
|
|
bootPrefs.getBoolean("first_boot", false)
|
|
}
|
|
// Force initialize language early.
|
|
LanguageSwitcher(this)
|
|
updateTheme()
|
|
if (!BuildConfig.DEBUG && isDeveloper) {
|
|
Timber.e("Developer mode is enabled! Support will not be provided if you are not a developer!")
|
|
isTainted = true
|
|
}
|
|
// Update emoji config
|
|
val fontRequestEmojiCompatConfig = DefaultEmojiCompatConfig.create(this)
|
|
if (fontRequestEmojiCompatConfig != null) {
|
|
fontRequestEmojiCompatConfig.setReplaceAll(true)
|
|
fontRequestEmojiCompatConfig.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL)
|
|
val emojiCompat = EmojiCompat.init(fontRequestEmojiCompatConfig)
|
|
Thread({
|
|
if (forceDebugLogging) Timber.i("Loading emoji compat...")
|
|
emojiCompat.load()
|
|
if (forceDebugLogging) Timber.i("Emoji compat loaded!")
|
|
}, "Emoji compat init.").start()
|
|
}
|
|
@Suppress("KotlinConstantConditions") if ((BuildConfig.ANDROIDACY_CLIENT_ID == "")) {
|
|
Timber.w("Androidacy client id is empty! Please set it in androidacy.properties. Will not enable Androidacy.")
|
|
val editor = sharedPreferences!!.edit()
|
|
editor.putBoolean("pref_androidacy_repo_enabled", false)
|
|
Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 1")
|
|
editor.apply()
|
|
}
|
|
}
|
|
|
|
private val intent: Intent?
|
|
get() = this.packageManager.getLaunchIntentForPackage(this.packageName)
|
|
|
|
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
|
val newTimeFormatLocale = newConfig.locales[0]
|
|
if (timeFormatLocale !== newTimeFormatLocale) {
|
|
timeFormatLocale = newTimeFormatLocale
|
|
timeFormat = SimpleDateFormat(TFS, timeFormatLocale)
|
|
}
|
|
super.onConfigurationChanged(newConfig)
|
|
}
|
|
|
|
// getDataDir wrapper with optional path parameter
|
|
@Suppress("NAME_SHADOWING")
|
|
fun getDataDirWithPath(path: String?): File {
|
|
var path = path
|
|
var dataDir = this.dataDir
|
|
// for path with / somewhere in the middle, its a subdirectory
|
|
return if (path != null) {
|
|
if (path.startsWith("/")) path = path.substring(1)
|
|
if (path.endsWith("/")) path = path.substring(0, path.length - 1)
|
|
if (path.contains("/")) {
|
|
val dirs = path.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
for (dir: String? in dirs) {
|
|
dataDir = File(dataDir, dir!!)
|
|
// make sure the directory exists
|
|
if (!dataDir.exists()) {
|
|
if (!dataDir.mkdirs()) {
|
|
if (forceDebugLogging) Timber.w(
|
|
"Failed to create directory %s", dataDir
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
dataDir = File(dataDir, path)
|
|
// create the directory if it doesn't exist
|
|
if (!dataDir.exists()) {
|
|
if (!dataDir.mkdirs()) {
|
|
if (forceDebugLogging) Timber.w("Failed to create directory %s", dataDir)
|
|
}
|
|
}
|
|
}
|
|
dataDir
|
|
} else {
|
|
throw IllegalArgumentException("Path cannot be null")
|
|
}
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi") // view is nullable because it's called from xml
|
|
fun resetApp() {
|
|
// cant show a dialog because android is throwing a fit so here's hoping anybody who calls this method is otherwise confirming that the user wants to reset the app
|
|
Timber.w("Resetting app...")
|
|
// recursively delete the app's data
|
|
(this.getSystemService(ACTIVITY_SERVICE) as ActivityManager).clearApplicationUserData()
|
|
}
|
|
|
|
// determine if the app is in the foreground
|
|
val isInForeground: Boolean
|
|
get() {
|
|
// determine if the app is in the foreground
|
|
val activityManager = this.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
|
val appProcesses = activityManager.runningAppProcesses
|
|
if (appProcesses == null) {
|
|
if (forceDebugLogging) Timber.d("appProcesses is null")
|
|
return false
|
|
}
|
|
val packageName = this.packageName
|
|
for (appProcess in appProcesses) {
|
|
if (forceDebugLogging) Timber.d(
|
|
"Process: %s, Importance: %d", appProcess.processName, appProcess.importance
|
|
)
|
|
if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// returns if background execution is restricted
|
|
val isBackgroundRestricted: Boolean
|
|
get() {
|
|
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
am.isBackgroundRestricted
|
|
} else {
|
|
false
|
|
}
|
|
}// sleep until the key is made
|
|
|
|
|
|
fun resetUpdateModule() {
|
|
modulesHaveUpdates = false
|
|
updateModuleCount = 0
|
|
updateModules = ArrayList()
|
|
}
|
|
|
|
class ReleaseTree : Timber.Tree() {
|
|
@SuppressLint("LogNotTimber")
|
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
|
// basically silently drop all logs below error, and write the rest to logcat
|
|
@Suppress("NAME_SHADOWING") var message = message
|
|
if (INSTANCE!!.isTainted) {
|
|
// prepend [TAINTED] to the message
|
|
message = "[TAINTED] $message"
|
|
}
|
|
if (forceDebugLogging) {
|
|
if (priority >= Log.DEBUG) {
|
|
when (priority) {
|
|
Log.DEBUG -> Log.d(tag, message, t)
|
|
Log.INFO -> Log.i(tag, message, t)
|
|
Log.WARN -> Log.w(tag, message, t)
|
|
Log.ERROR -> Log.e(tag, message, t)
|
|
Log.ASSERT -> Log.wtf(tag, message, t)
|
|
}
|
|
t?.printStackTrace()
|
|
}
|
|
} else {
|
|
if (priority >= Log.ERROR) {
|
|
Log.e(tag, message, t)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
|
|
var forceDebugLogging: Boolean =
|
|
BuildConfig.DEBUG || getPreferences("mmm")?.getBoolean(
|
|
"pref_force_debug_logging",
|
|
false
|
|
) ?: false
|
|
|
|
// Warning! Locales that don't exist will crash the app
|
|
// Anything that is commented out is supported but the translation is not complete to at least 60%
|
|
@JvmField
|
|
val supportedLocales = HashSet<String>()
|
|
private const val TFS = "dd MMM yyyy" // Example: 13 july 2001
|
|
private var shellBuilder: Shell.Builder? = null
|
|
|
|
// Is application wrapped, and therefore must reduce it's feature set.
|
|
@SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper.
|
|
const val isWrapped = false
|
|
private val callers = ArrayList<String>()
|
|
|
|
@JvmField
|
|
var o = false
|
|
private var SHOWCASE_MODE_TRUE: String? = null
|
|
private var sc: Long = 0
|
|
private var timeFormatLocale = Resources.getSystem().configuration.locales[0]
|
|
private var timeFormat = SimpleDateFormat(TFS, timeFormatLocale)
|
|
private var relPackageName = BuildConfig.APPLICATION_ID
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
var INSTANCE: MainApplication? = null
|
|
private set
|
|
get() {
|
|
if (field == null) {
|
|
Timber.w("MainApplication.INSTANCE is null, using fallback!")
|
|
return null
|
|
}
|
|
return field
|
|
}
|
|
|
|
var isFirstBoot = false
|
|
private var mSharedPrefs: HashMap<Any, Any>? = null
|
|
var updateCheckBg: String? = null
|
|
|
|
init {
|
|
Shell.setDefaultBuilder(
|
|
Shell.Builder.create()
|
|
.setFlags(Shell.FLAG_REDIRECT_STDERR or Shell.FLAG_MOUNT_MASTER).setTimeout(15)
|
|
)
|
|
// set verbose logging for debug builds
|
|
if (BuildConfig.DEBUG) {
|
|
Shell.enableVerboseLogging = true
|
|
}
|
|
// prewarm shell
|
|
Shell.getShell {
|
|
if (forceDebugLogging) Timber.d("Shell prewarmed")
|
|
}
|
|
val random = Random()
|
|
do {
|
|
sc = random.nextLong()
|
|
} while (sc == 0L)
|
|
}
|
|
|
|
fun build(vararg command: String?): Shell {
|
|
return shellBuilder!!.build(*command)
|
|
}
|
|
|
|
fun addSecret(intent: Intent) {
|
|
val componentName = intent.component
|
|
val packageName = componentName?.packageName ?: (intent.getPackage())!!
|
|
require(
|
|
!(!BuildConfig.APPLICATION_ID.equals(
|
|
packageName, ignoreCase = true
|
|
) && relPackageName != packageName)
|
|
) {
|
|
// Code safeguard, we should never reach here.
|
|
"Can't add secret to outbound Intent"
|
|
}
|
|
intent.putExtra("secret", sc)
|
|
}
|
|
|
|
@Suppress("NAME_SHADOWING")
|
|
fun getPreferences(name: String): SharedPreferences? {
|
|
// encryptedSharedPreferences is used
|
|
return try {
|
|
var name = name
|
|
val mContext: Context? = INSTANCE!!.applicationContext
|
|
name += "x"
|
|
if (mSharedPrefs == null) {
|
|
mSharedPrefs = HashMap()
|
|
}
|
|
if (mSharedPrefs!!.containsKey(name)) {
|
|
return mSharedPrefs!![name] as SharedPreferences?
|
|
}
|
|
val masterKey =
|
|
MasterKey.Builder(mContext!!).setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
.build()
|
|
val sharedPreferences = EncryptedSharedPreferences.create(
|
|
mContext,
|
|
name,
|
|
masterKey,
|
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
)
|
|
mSharedPrefs!![name] = sharedPreferences
|
|
sharedPreferences
|
|
} catch (e: Exception) {
|
|
// try again five times, with a 250ms delay between each try. if we still can't get the shared preferences, throw an exception
|
|
var i = 0
|
|
while (i < 5) {
|
|
try {
|
|
Thread.sleep(250)
|
|
} catch (ignored: InterruptedException) {
|
|
}
|
|
try {
|
|
val masterKey =
|
|
MasterKey.Builder(INSTANCE!!.applicationContext)
|
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
.build()
|
|
val sharedPreferences = EncryptedSharedPreferences.create(
|
|
INSTANCE!!.applicationContext,
|
|
name,
|
|
masterKey,
|
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
)
|
|
mSharedPrefs!![name] = sharedPreferences
|
|
return sharedPreferences
|
|
} catch (e: Exception) {
|
|
Timber.e(e, "Failed to get shared preferences")
|
|
}
|
|
i++
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|
|
fun clearCachedSharedPrefs() {
|
|
mSharedPrefs = null
|
|
}
|
|
|
|
fun checkSecret(intent: Intent?): Boolean {
|
|
return intent != null && intent.getLongExtra("secret", sc.inv()) == sc
|
|
}
|
|
|
|
// convert from String to boolean
|
|
val isShowcaseMode: Boolean
|
|
get() {
|
|
if (SHOWCASE_MODE_TRUE != null) {
|
|
// convert from String to boolean
|
|
return java.lang.Boolean.parseBoolean(SHOWCASE_MODE_TRUE)
|
|
}
|
|
val showcaseMode =
|
|
getPreferences("mmm")!!.getBoolean("pref_showcase_mode", false)
|
|
SHOWCASE_MODE_TRUE = showcaseMode.toString()
|
|
return showcaseMode
|
|
}
|
|
|
|
fun shouldPreventReboot(): Boolean {
|
|
return getPreferences("mmm")!!.getBoolean("pref_prevent_reboot", true)
|
|
}
|
|
|
|
val isShowIncompatibleModules: Boolean
|
|
get() = getPreferences("mmm")!!.getBoolean("pref_show_incompatible", false)
|
|
val isForceDarkTerminal: Boolean
|
|
get() = getPreferences("mmm")!!.getBoolean("pref_force_dark_terminal", false)
|
|
val isTextWrapEnabled: Boolean
|
|
get() = getPreferences("mmm")!!.getBoolean("pref_wrap_text", false)
|
|
val isDohEnabled: Boolean
|
|
get() = getPreferences("mmm")!!.getBoolean("pref_dns_over_https", true)
|
|
val isMonetEnabled: Boolean
|
|
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getPreferences("mmm")!!.getBoolean(
|
|
"pref_enable_monet", true
|
|
)
|
|
val isBlurEnabled: Boolean
|
|
get() = getPreferences("mmm")!!.getBoolean("pref_enable_blur", false)
|
|
|
|
val isDeveloper: Boolean
|
|
get() {
|
|
return if (BuildConfig.DEBUG) true else getPreferences("mmm")!!.getBoolean(
|
|
"developer", false
|
|
)
|
|
}
|
|
val isDisableLowQualityModuleFilter: Boolean
|
|
get() = getPreferences("mmm")!!.getBoolean(
|
|
"pref_disable_low_quality_module_filter", false
|
|
) && isDeveloper
|
|
val isUsingMagiskCommand: Boolean
|
|
get() = (peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND) && getPreferences(
|
|
"mmm"
|
|
)!!.getBoolean(
|
|
"pref_use_magisk_install_command",
|
|
false
|
|
) && isDeveloper && !InstallerInitializer.isKsu
|
|
|
|
val isBackgroundUpdateCheckEnabled: Boolean
|
|
get() {
|
|
if (updateCheckBg != null) {
|
|
return java.lang.Boolean.parseBoolean(updateCheckBg)
|
|
}
|
|
val wrapped = isWrapped
|
|
val updateCheckBgTemp = !wrapped && getPreferences("mmm")!!.getBoolean(
|
|
"pref_background_update_check", true
|
|
)
|
|
updateCheckBg = updateCheckBgTemp.toString()
|
|
return java.lang.Boolean.parseBoolean(updateCheckBg)
|
|
}
|
|
val isAndroidacyTestMode: Boolean
|
|
get() = isDeveloper && getPreferences("mmm")!!.getBoolean(
|
|
"pref_androidacy_test_mode", false
|
|
)
|
|
|
|
fun setHasGottenRootAccess(bool: Boolean) {
|
|
getPreferences("mmm")!!.edit().putBoolean("has_root_access", bool).apply()
|
|
}
|
|
|
|
val isCrashReportingEnabled: Boolean
|
|
get() = analyticsAllowed() && getPreferences("mmm")!!.getBoolean(
|
|
"pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING
|
|
)
|
|
val bootSharedPreferences: SharedPreferences?
|
|
get() = getPreferences("mmm_boot")
|
|
|
|
fun formatTime(timeStamp: Long): String {
|
|
// new Date(x) also get the local timestamp for format
|
|
return timeFormat.format(Date(timeStamp))
|
|
}
|
|
|
|
val isNotificationPermissionGranted: Boolean
|
|
get() = NotificationManagerCompat.from((INSTANCE)!!).areNotificationsEnabled()
|
|
|
|
fun analyticsAllowed(): Boolean {
|
|
return getPreferences("mmm")!!.getBoolean(
|
|
"pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS
|
|
)
|
|
}
|
|
|
|
fun shouldShowFeedback(): Boolean {
|
|
// should not have been shown in 14 days and only 1 in 5 chance
|
|
if (!analyticsAllowed()) {
|
|
return false
|
|
}
|
|
val randChance = Random().nextInt(5)
|
|
val lastShown = getPreferences("mmm")!!.getLong("last_feedback", 0)
|
|
if (forceDebugLogging) Timber.d(
|
|
"Last feedback shown: %d, randChance: %d",
|
|
lastShown,
|
|
randChance
|
|
)
|
|
return System.currentTimeMillis() - lastShown > 1209600000 && randChance == 0
|
|
}
|
|
|
|
var dirty = false
|
|
}
|
|
|
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
|
lastActivity = activity as AppCompatActivity
|
|
activity.setTheme(managerThemeResId)
|
|
}
|
|
|
|
override fun onActivityStarted(activity: Activity) {
|
|
if (analyticsAllowed()) Countly.sharedInstance().onStart(activity)
|
|
}
|
|
|
|
override fun onActivityResumed(activity: Activity) {
|
|
lastActivity = activity as AppCompatActivity
|
|
activity.setTheme(managerThemeResId)
|
|
}
|
|
|
|
override fun onActivityPaused(activity: Activity) {
|
|
}
|
|
|
|
override fun onActivityStopped(activity: Activity) {
|
|
if (analyticsAllowed()) Countly.sharedInstance().onStop()
|
|
}
|
|
|
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
|
}
|
|
|
|
override fun onActivityDestroyed(activity: Activity) {
|
|
}
|
|
}
|