/ *
* 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.Process
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 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 ( ) {
Thread . setDefaultUncaughtExceptionHandler { _ : Thread ? , throwable : Throwable ->
clearCachedSharedPrefs ( )
// open crash handler and exit
val intent = Intent ( this , CrashHandler :: class . java )
// pass the entire exception to the crash handler
intent . putExtra ( " exception " , throwable )
// add stacktrace as string
intent . putExtra ( " stacktrace " , throwable . stackTrace )
// serialize Sentry.captureException and pass it to the crash handler
intent . putExtra ( " sentryException " , throwable )
// pass crashReportingEnabled to crash handler
intent . putExtra ( " crashReportingEnabled " , isCrashReportingEnabled )
// add isCrashing to intent
intent . putExtra ( " isCrashing " , true )
intent . addFlags ( Intent . FLAG _ACTIVITY _NEW _TASK or Intent . FLAG _ACTIVITY _CLEAR _TASK )
Timber . e ( " Starting crash handler " )
startActivity ( intent )
Timber . e ( " Exiting " )
Process . killProcess ( Process . myPid ( ) )
}
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
super . onCreate ( )
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 ( !is CrashHandler ) {
val fileUtils = FileUtils ( )
fileUtils . ensureCacheDirs ( )
fileUtils . ensureURLHandler ( this )
}
if ( forceDebugLogging ) Timber . d ( " Initializing AMM " )
if ( forceDebugLogging ) Timber . d ( " Started from background: %s " , !is InForeground )
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
var s = false
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
s = true
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 ( ) = 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
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
}
}
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 ) {
}
}