finish room and kotlin migration

SettingsActivity.java cannot be converted right now because AS crashes silently

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/89/head
androidacy-user 2 years ago
parent 0b05dfe29b
commit 743cd3f46d

@ -72,6 +72,10 @@ android {
"en" "en"
) )
) )
// ksp room processor
room {
schemaLocationDir.set(file("roomSchemas"))
}
} }
splits { splits {
@ -525,9 +529,6 @@ dependencies {
// yes // yes
implementation("com.github.fingerprintjs:fingerprint-android:2.0.0") implementation("com.github.fingerprintjs:fingerprint-android:2.0.0")
// encryption for room
implementation("net.zetetic:android-database-sqlcipher:4.5.4")
// room // room
implementation("androidx.room:room-runtime:2.5.1") implementation("androidx.room:room-runtime:2.5.1")

@ -0,0 +1,142 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "2abfd2dad332c887d96cdda63d92a639",
"entities": [
{
"tableName": "modulelistcache",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codename` TEXT NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `author` TEXT NOT NULL, `description` TEXT NOT NULL, `minApi` INTEGER NOT NULL, `maxApi` INTEGER NOT NULL, `minMagisk` INTEGER NOT NULL, `needRamdisk` INTEGER NOT NULL, `support` TEXT NOT NULL, `donate` TEXT NOT NULL, `config` TEXT NOT NULL, `changeBoot` INTEGER NOT NULL, `mmtReborn` INTEGER NOT NULL, `repoId` TEXT NOT NULL, `lastUpdate` INTEGER NOT NULL, `name` TEXT NOT NULL, `safe` INTEGER NOT NULL, `stats` INTEGER NOT NULL, PRIMARY KEY(`codename`))",
"fields": [
{
"fieldPath": "codename",
"columnName": "codename",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionCode",
"columnName": "versionCode",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "minApi",
"columnName": "minApi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maxApi",
"columnName": "maxApi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minMagisk",
"columnName": "minMagisk",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "needRamdisk",
"columnName": "needRamdisk",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "support",
"columnName": "support",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "donate",
"columnName": "donate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "config",
"columnName": "config",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "changeBoot",
"columnName": "changeBoot",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mmtReborn",
"columnName": "mmtReborn",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "repoId",
"columnName": "repoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUpdate",
"columnName": "lastUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "safe",
"columnName": "safe",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stats",
"columnName": "stats",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"codename"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2abfd2dad332c887d96cdda63d92a639')"
]
}
}

@ -0,0 +1,82 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "5a217dbf3caa5a6b70e7eee98636866c",
"entities": [
{
"tableName": "ReposList",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `donate` TEXT, `support` TEXT, `submitModule` TEXT, `lastUpdate` INTEGER NOT NULL, `name` TEXT NOT NULL, `website` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "donate",
"columnName": "donate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "support",
"columnName": "support",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "submitModule",
"columnName": "submitModule",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastUpdate",
"columnName": "lastUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5a217dbf3caa5a6b70e7eee98636866c')"
]
}
}

Binary file not shown.

@ -28,7 +28,6 @@ import androidx.appcompat.widget.SearchView
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.room.Room
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.fox2code.foxcompat.app.FoxActivity import com.fox2code.foxcompat.app.FoxActivity
@ -55,7 +54,6 @@ import com.fox2code.mmm.utils.RuntimeUtils
import com.fox2code.mmm.utils.SyncManager import com.fox2code.mmm.utils.SyncManager
import com.fox2code.mmm.utils.io.net.Http.Companion.cleanDnsCache import com.fox2code.mmm.utils.io.net.Http.Companion.cleanDnsCache
import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
import com.fox2code.mmm.utils.room.ReposListDatabase
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import org.matomo.sdk.extra.TrackHelper import org.matomo.sdk.extra.TrackHelper
@ -102,25 +100,6 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
onMainActivityCreate(this) onMainActivityCreate(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker) TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
// track enabled repos
val db = Room.databaseBuilder(
applicationContext,
ReposListDatabase::class.java,
"reposlist.db"
).build()
val repoDao = db.reposListDao()
val repos = repoDao.getAll()
val enabledRepos = StringBuilder()
for (repo in repos) {
if (repo.enabled) {
enabledRepos.append(repo.url).append(", ")
}
}
if (enabledRepos.isNotEmpty()) {
enabledRepos.delete(enabledRepos.length - 2, enabledRepos.length)
TrackHelper.track().event("Enabled Repos", enabledRepos.toString())
.with(MainApplication.INSTANCE!!.tracker)
}
// hide this behind a buildconfig flag for now, but crash the app if it's not an official build and not debug // hide this behind a buildconfig flag for now, but crash the app if it's not an official build and not debug
if (BuildConfig.ENABLE_PROTECTION && !MainApplication.o && !BuildConfig.DEBUG) { if (BuildConfig.ENABLE_PROTECTION && !MainApplication.o && !BuildConfig.DEBUG) {
throw RuntimeException("This is not an official build of AMM") throw RuntimeException("This is not an official build of AMM")

@ -14,9 +14,6 @@ import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log import android.util.Log
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -44,35 +41,16 @@ import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
import io.realm.Realm
import org.matomo.sdk.Matomo import org.matomo.sdk.Matomo
import org.matomo.sdk.Tracker import org.matomo.sdk.Tracker
import org.matomo.sdk.TrackerBuilder import org.matomo.sdk.TrackerBuilder
import org.matomo.sdk.extra.TrackHelper import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.NoSuchAlgorithmException
import java.security.NoSuchProviderException
import java.security.SecureRandom import java.security.SecureRandom
import java.security.UnrecoverableKeyException
import java.security.cert.CertificateException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Random import java.util.Random
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.KeyGenerator
import javax.crypto.NoSuchPaddingException
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
@ -94,7 +72,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
@JvmField @JvmField
var markwon: Markwon? = null var markwon: Markwon? = null
private var existingKey: ByteArray? = null private var existingKey: CharArray? = null
@JvmField @JvmField
var tracker: Tracker? = null var tracker: Tracker? = null
@ -106,6 +84,38 @@ class MainApplication : FoxApplication(), Configuration.Provider {
INSTANCE = this 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!!
}
fun getMarkwon(): Markwon? { fun getMarkwon(): Markwon? {
if (isCrashHandler) return null if (isCrashHandler) return null
if (markwon != null) return markwon if (markwon != null) return markwon
@ -178,8 +188,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
@SuppressLint("NonConstantResourceId") @SuppressLint("NonConstantResourceId")
override fun isLightTheme(): Boolean { override fun isLightTheme(): Boolean {
return when (getSharedPreferences("mmm")!! return when (getSharedPreferences("mmm")!!.getString("pref_theme", "system")) {
.getString("pref_theme", "system")) {
"system" -> isSystemLightTheme "system" -> isSystemLightTheme
"dark", "black" -> false "dark", "black" -> false
else -> true else -> true
@ -253,9 +262,6 @@ class MainApplication : FoxApplication(), Configuration.Provider {
Timber.d("Initializing AMM") Timber.d("Initializing AMM")
Timber.d("Started from background: %s", !isInForeground) Timber.d("Started from background: %s", !isInForeground)
Timber.d("AMM is running in debug mode") Timber.d("AMM is running in debug mode")
Timber.d("Initializing Realm")
Realm.init(this)
Timber.d("Initialized Realm")
// analytics // analytics
Timber.d("Initializing matomo") Timber.d("Initializing matomo")
getTracker() getTracker()
@ -265,21 +271,17 @@ class MainApplication : FoxApplication(), Configuration.Provider {
} else { } else {
tracker!!.isOptOut = false tracker!!.isOptOut = false
} }
if (getSharedPreferences("matomo")!! if (getSharedPreferences("matomo")!!.getBoolean("install_tracked", false)) {
.getBoolean("install_tracked", false)
) {
TrackHelper.track().download().with(INSTANCE!!.getTracker()) TrackHelper.track().download().with(INSTANCE!!.getTracker())
Timber.d("Sent install event to matomo") Timber.d("Sent install event to matomo")
getSharedPreferences("matomo")!! getSharedPreferences("matomo")!!.edit().putBoolean("install_tracked", true).apply()
.edit().putBoolean("install_tracked", true).apply()
} else { } else {
Timber.d("Matomo already has install") Timber.d("Matomo already has install")
} }
try { try {
@Suppress("DEPRECATION") @Suppress("DEPRECATION") @SuppressLint("PackageManagerGetSignatures") val s =
@SuppressLint("PackageManagerGetSignatures") val s = this.packageManager.getPackageInfo( this.packageManager.getPackageInfo(
this.packageName, this.packageName, PackageManager.GET_SIGNATURES
PackageManager.GET_SIGNATURES
).signatures ).signatures
@Suppress("SpellCheckingInspection") val osh = arrayOf( @Suppress("SpellCheckingInspection") val osh = arrayOf(
"7bec7c4462f4aac616612d9f56a023ee3046e83afa956463b5fab547fd0a0be6", "7bec7c4462f4aac616612d9f56a023ee3046e83afa956463b5fab547fd0a0be6",
@ -322,8 +324,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
Timber.i("Emoji compat loaded!") Timber.i("Emoji compat loaded!")
}, "Emoji compat init.").start() }, "Emoji compat init.").start()
} }
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions") if ((BuildConfig.ANDROIDACY_CLIENT_ID == "")) {
if ((BuildConfig.ANDROIDACY_CLIENT_ID == "")) {
Timber.w("Androidacy client id is empty! Please set it in androidacy.properties. Will not enable Androidacy.") Timber.w("Androidacy client id is empty! Please set it in androidacy.properties. Will not enable Androidacy.")
val editor = sharedPreferences!!.edit() val editor = sharedPreferences!!.edit()
editor.putBoolean("pref_androidacy_repo_enabled", false) editor.putBoolean("pref_androidacy_repo_enabled", false)
@ -371,8 +372,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
if (!dataDir.exists()) { if (!dataDir.exists()) {
if (!dataDir.mkdirs()) { if (!dataDir.mkdirs()) {
if (BuildConfig.DEBUG) Timber.w( if (BuildConfig.DEBUG) Timber.w(
"Failed to create directory %s", "Failed to create directory %s", dataDir
dataDir
) )
} }
} }
@ -413,9 +413,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
val packageName = this.packageName val packageName = this.packageName
for (appProcess in appProcesses) { for (appProcess in appProcesses) {
Timber.d( Timber.d(
"Process: %s, Importance: %d", "Process: %s, Importance: %d", appProcess.processName, appProcess.importance
appProcess.processName,
appProcess.importance
) )
if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) { if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) {
return true return true
@ -435,237 +433,6 @@ class MainApplication : FoxApplication(), Configuration.Provider {
} }
}// sleep until the key is made }// sleep until the key is made
// attempt to read the existingKey property
// check if we have a key already
// open a connection to the android keystore
// create a securely generated random asymmetric RSA key
// create a cipher that uses AES encryption -- we'll use this to encrypt our key
// generate secret key
// access the generated key in the android keystore, then
// use the cipher to create an encrypted version of the key
// keep the encrypted key in shared preferences
// to persist it across application runs
// pass to a realm configuration via encryptionKey()
// Create a key to encrypt a realm and save it securely in the keystore
val key: ByteArray
get() {
if (makingNewKey) {
// sleep until the key is made
while (makingNewKey) try {
Thread.sleep(100)
} catch (ignored: InterruptedException) {
Thread.currentThread().interrupt()
}
}
// attempt to read the existingKey property
if (existingKey != null && existingKey!!.isNotEmpty()) {
return existingKey as ByteArray
}
// check if we have a key already
val sharedPreferences = getSharedPreferences("realm_key")
makingNewKey = if (sharedPreferences!!.contains("iv_and_encrypted_key")) {
return getExistingKey()
} else {
true
}
// open a connection to the android keystore
val keyStore: KeyStore
try {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
} catch (e: KeyStoreException) {
Timber.v("Failed to open the keystore.")
throw RuntimeException(e)
} catch (e: NoSuchAlgorithmException) {
Timber.v("Failed to open the keystore.")
throw RuntimeException(e)
} catch (e: CertificateException) {
Timber.v("Failed to open the keystore.")
throw RuntimeException(e)
} catch (e: IOException) {
Timber.v("Failed to open the keystore.")
throw RuntimeException(e)
}
// create a securely generated random asymmetric RSA key
val realmKey = ByteArray(Realm.ENCRYPTION_KEY_LENGTH)
do {
SecureRandom().nextBytes(realmKey)
} while (realmKey[0].toInt() == 0)
// create a cipher that uses AES encryption -- we'll use this to encrypt our key
val cipher: Cipher = try {
Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7)
} catch (e: NoSuchAlgorithmException) {
Timber.e("Failed to create a cipher.")
throw RuntimeException(e)
} catch (e: NoSuchPaddingException) {
Timber.e("Failed to create a cipher.")
throw RuntimeException(e)
}
Timber.v("Cipher created.")
// generate secret key
val keyGenerator: KeyGenerator = try {
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
} catch (e: NoSuchAlgorithmException) {
Timber.e("Failed to access the key generator.")
throw RuntimeException(e)
} catch (e: NoSuchProviderException) {
Timber.e("Failed to access the key generator.")
throw RuntimeException(e)
}
val keySpec = KeyGenParameterSpec.Builder(
"realm_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7).build()
try {
keyGenerator.init(keySpec)
} catch (e: InvalidAlgorithmParameterException) {
Timber.e("Failed to generate a secret key.")
throw RuntimeException(e)
}
Timber.v("Secret key generated.")
keyGenerator.generateKey()
Timber.v("Secret key stored in the keystore.")
// access the generated key in the android keystore, then
// use the cipher to create an encrypted version of the key
val initializationVector: ByteArray
val encryptedKeyForRealm: ByteArray
try {
val secretKey = keyStore.getKey("realm_key", null) as SecretKey
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
encryptedKeyForRealm = cipher.doFinal(realmKey)
initializationVector = cipher.iv
} catch (e: InvalidKeyException) {
Timber.e("Failed encrypting the key with the secret key.")
throw RuntimeException(e)
} catch (e: UnrecoverableKeyException) {
Timber.e("Failed encrypting the key with the secret key.")
throw RuntimeException(e)
} catch (e: NoSuchAlgorithmException) {
Timber.e("Failed encrypting the key with the secret key.")
throw RuntimeException(e)
} catch (e: KeyStoreException) {
Timber.e("Failed encrypting the key with the secret key.")
throw RuntimeException(e)
} catch (e: BadPaddingException) {
Timber.e("Failed encrypting the key with the secret key.")
throw RuntimeException(e)
} catch (e: IllegalBlockSizeException) {
Timber.e("Failed encrypting the key with the secret key.")
throw RuntimeException(e)
}
// keep the encrypted key in shared preferences
// to persist it across application runs
val initializationVectorAndEncryptedKey =
ByteArray(Integer.BYTES + initializationVector.size + encryptedKeyForRealm.size)
val buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey)
buffer.order(ByteOrder.BIG_ENDIAN)
buffer.putInt(initializationVector.size)
buffer.put(initializationVector)
buffer.put(encryptedKeyForRealm)
Timber.d("Created all keys successfully.")
getSharedPreferences("realm_key")!!
.edit().putString(
"iv_and_encrypted_key",
Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)
).apply()
Timber.d("Saved the encrypted key in shared preferences.")
makingNewKey = false
Timber.d("Returning the new key. Key length: %d", realmKey.size)
return realmKey // pass to a realm configuration via encryptionKey()
}
// Access the encrypted key in the keystore, decrypt it with the secret,
// and use it to open and read from the realm again
fun getExistingKey(): ByteArray {
Timber.v("Getting existing key.")
// attempt to read the existingKey property
if (existingKey != null) {
Timber.v("Existing key found in memory.")
return existingKey as ByteArray
}
// open a connection to the android keystore
val keyStore: KeyStore
try {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
} catch (e: KeyStoreException) {
Timber.e("Failed to open the keystore.")
throw RuntimeException(e)
} catch (e: NoSuchAlgorithmException) {
Timber.e("Failed to open the keystore.")
throw RuntimeException(e)
} catch (e: CertificateException) {
Timber.e("Failed to open the keystore.")
throw RuntimeException(e)
} catch (e: IOException) {
Timber.e("Failed to open the keystore.")
throw RuntimeException(e)
}
Timber.v("Keystore opened.")
// access the encrypted key that's stored in shared preferences
val initializationVectorAndEncryptedKey = Base64.decode(
getSharedPreferences("realm_key")!!
.getString("iv_and_encrypted_key", null), Base64.DEFAULT
)
Timber.d(
"Retrieved the encrypted key from shared preferences. Key length: %d",
initializationVectorAndEncryptedKey.size
)
val buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey)
buffer.order(ByteOrder.BIG_ENDIAN)
// extract the length of the initialization vector from the buffer
val initializationVectorLength = buffer.int
// extract the initialization vector based on that length
val initializationVector = ByteArray(initializationVectorLength)
buffer[initializationVector]
// extract the encrypted key
val encryptedKey =
ByteArray(initializationVectorAndEncryptedKey.size - Integer.BYTES - initializationVectorLength)
buffer[encryptedKey]
Timber.d("Got key from shared preferences.")
// create a cipher that uses AES encryption to decrypt our key
val cipher: Cipher = try {
Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7)
} catch (e: NoSuchAlgorithmException) {
Timber.e("Failed to create cipher.")
throw RuntimeException(e)
} catch (e: NoSuchPaddingException) {
Timber.e("Failed to create cipher.")
throw RuntimeException(e)
}
// decrypt the encrypted key with the secret key stored in the keystore
val decryptedKey: ByteArray = try {
val secretKey = keyStore.getKey("realm_key", null) as SecretKey
val initializationVectorSpec = IvParameterSpec(initializationVector)
cipher.init(Cipher.DECRYPT_MODE, secretKey, initializationVectorSpec)
cipher.doFinal(encryptedKey)
} catch (e: InvalidKeyException) {
Timber.e("Failed to decrypt. Invalid key.")
throw RuntimeException(e)
} catch (e: UnrecoverableKeyException) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.")
throw RuntimeException(e)
} catch (e: NoSuchAlgorithmException) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.")
throw RuntimeException(e)
} catch (e: BadPaddingException) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.")
throw RuntimeException(e)
} catch (e: KeyStoreException) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.")
throw RuntimeException(e)
} catch (e: IllegalBlockSizeException) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.")
throw RuntimeException(e)
} catch (e: InvalidAlgorithmParameterException) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.")
throw RuntimeException(e)
}
// set property on MainApplication to indicate that the key has been accessed
existingKey = decryptedKey
return decryptedKey // pass to a realm configuration via encryptionKey()
}
fun resetUpdateModule() { fun resetUpdateModule() {
modulesHaveUpdates = false modulesHaveUpdates = false
@ -728,8 +495,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
var updateCheckBg: String? = null var updateCheckBg: String? = null
init { init {
Shell.setDefaultBuilder( Shell.setDefaultBuilder(Shell.Builder.create()
Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR or Shell.FLAG_MOUNT_MASTER).setTimeout(15) .setFlags(Shell.FLAG_REDIRECT_STDERR or Shell.FLAG_MOUNT_MASTER).setTimeout(15)
.setInitializers( .setInitializers(
InstallerInitializer::class.java InstallerInitializer::class.java
@ -749,8 +515,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
val packageName = componentName?.packageName ?: (intent.getPackage())!! val packageName = componentName?.packageName ?: (intent.getPackage())!!
require( require(
!(!BuildConfig.APPLICATION_ID.equals( !(!BuildConfig.APPLICATION_ID.equals(
packageName, packageName, ignoreCase = true
ignoreCase = true
) && relPackageName != packageName) ) && relPackageName != packageName)
) { ) {
// Code safeguard, we should never reach here. // Code safeguard, we should never reach here.
@ -772,7 +537,8 @@ class MainApplication : FoxApplication(), Configuration.Provider {
} }
/* /*
this part is only here because with added encryption, parts of code that were previously calling this over and over again or on each invocation of a method are causing performance issues. this part is only here because with added encryption, parts of code that were previously calling this over and over again or on each invocation of a method are causing performance issues.
*/if (BuildConfig.DEBUG) { */
if (BuildConfig.DEBUG) {
// get file, function, and line number // get file, function, and line number
val stackTrace = Thread.currentThread().stackTrace val stackTrace = Thread.currentThread().stackTrace
// get the caller of this method // get the caller of this method
@ -787,8 +553,7 @@ class MainApplication : FoxApplication(), Configuration.Provider {
// add the caller to an array. if the last 3 callers are the same, then we are in a loop, log at error level // add the caller to an array. if the last 3 callers are the same, then we are in a loop, log at error level
callers.add(name + ":" + caller.lineNumber + ":" + caller.methodName) callers.add(name + ":" + caller.lineNumber + ":" + caller.methodName)
// get the last 3 callers // get the last 3 callers
val last3: List<String> = val last3: List<String> = callers.subList(max(callers.size - 3, 0), callers.size)
callers.subList(max(callers.size - 3, 0), callers.size)
// if the last 3 callers are the same, then we are in a loop, log at error level // if the last 3 callers are the same, then we are in a loop, log at error level
if (((last3.size == 3) && last3[0] == last3[1]) && last3[1] == last3[2]) { if (((last3.size == 3) && last3[0] == last3[1]) && last3[1] == last3[2]) {
Timber.e( Timber.e(
@ -834,50 +599,49 @@ class MainApplication : FoxApplication(), Configuration.Provider {
// convert from String to boolean // convert from String to boolean
return java.lang.Boolean.parseBoolean(SHOWCASE_MODE_TRUE) return java.lang.Boolean.parseBoolean(SHOWCASE_MODE_TRUE)
} }
val showcaseMode = getSharedPreferences("mmm")!! val showcaseMode =
.getBoolean("pref_showcase_mode", false) getSharedPreferences("mmm")!!.getBoolean("pref_showcase_mode", false)
SHOWCASE_MODE_TRUE = showcaseMode.toString() SHOWCASE_MODE_TRUE = showcaseMode.toString()
return showcaseMode return showcaseMode
} }
fun shouldPreventReboot(): Boolean { fun shouldPreventReboot(): Boolean {
return getSharedPreferences("mmm")!! return getSharedPreferences("mmm")!!.getBoolean("pref_prevent_reboot", true)
.getBoolean("pref_prevent_reboot", true)
} }
val isShowIncompatibleModules: Boolean val isShowIncompatibleModules: Boolean
get() = getSharedPreferences("mmm")!! get() = getSharedPreferences("mmm")!!.getBoolean("pref_show_incompatible", false)
.getBoolean("pref_show_incompatible", false)
val isForceDarkTerminal: Boolean val isForceDarkTerminal: Boolean
get() = getSharedPreferences("mmm")!! get() = getSharedPreferences("mmm")!!.getBoolean("pref_force_dark_terminal", false)
.getBoolean("pref_force_dark_terminal", false)
val isTextWrapEnabled: Boolean val isTextWrapEnabled: Boolean
get() = getSharedPreferences("mmm")!! get() = getSharedPreferences("mmm")!!.getBoolean("pref_wrap_text", false)
.getBoolean("pref_wrap_text", false)
val isDohEnabled: Boolean val isDohEnabled: Boolean
get() = getSharedPreferences("mmm")!! get() = getSharedPreferences("mmm")!!.getBoolean("pref_dns_over_https", true)
.getBoolean("pref_dns_over_https", true)
val isMonetEnabled: Boolean val isMonetEnabled: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences("mmm")!! get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("pref_enable_monet", true) "pref_enable_monet",
true
)
val isBlurEnabled: Boolean val isBlurEnabled: Boolean
get() = getSharedPreferences("mmm")!! get() = getSharedPreferences("mmm")!!.getBoolean("pref_enable_blur", false)
.getBoolean("pref_enable_blur", false)
@JvmStatic @JvmStatic
val isDeveloper: Boolean val isDeveloper: Boolean
get() { get() {
return if (BuildConfig.DEBUG) true else getSharedPreferences("mmm")!! return if (BuildConfig.DEBUG) true else getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("developer", false) "developer",
false
)
} }
val isDisableLowQualityModuleFilter: Boolean val isDisableLowQualityModuleFilter: Boolean
get() = getSharedPreferences("mmm")!! get() = getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("pref_disable_low_quality_module_filter", false) && isDeveloper "pref_disable_low_quality_module_filter",
false
) && isDeveloper
val isUsingMagiskCommand: Boolean val isUsingMagiskCommand: Boolean
get() = (peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND) && getSharedPreferences( get() = (peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND) && getSharedPreferences(
"mmm" "mmm"
)!! )!!.getBoolean("pref_use_magisk_install_command", false) && isDeveloper
.getBoolean("pref_use_magisk_install_command", false) && isDeveloper
@JvmStatic @JvmStatic
val isBackgroundUpdateCheckEnabled: Boolean val isBackgroundUpdateCheckEnabled: Boolean
@ -886,23 +650,28 @@ class MainApplication : FoxApplication(), Configuration.Provider {
return java.lang.Boolean.parseBoolean(updateCheckBg) return java.lang.Boolean.parseBoolean(updateCheckBg)
} }
val wrapped = isWrapped val wrapped = isWrapped
val updateCheckBgTemp = !wrapped && getSharedPreferences("mmm")!! val updateCheckBgTemp = !wrapped && getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("pref_background_update_check", true) "pref_background_update_check",
true
)
updateCheckBg = updateCheckBgTemp.toString() updateCheckBg = updateCheckBgTemp.toString()
return java.lang.Boolean.parseBoolean(updateCheckBg) return java.lang.Boolean.parseBoolean(updateCheckBg)
} }
val isAndroidacyTestMode: Boolean val isAndroidacyTestMode: Boolean
get() = isDeveloper && getSharedPreferences("mmm")!! get() = isDeveloper && getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("pref_androidacy_test_mode", false) "pref_androidacy_test_mode",
false
)
fun setHasGottenRootAccess(bool: Boolean) { fun setHasGottenRootAccess(bool: Boolean) {
getSharedPreferences("mmm")!! getSharedPreferences("mmm")!!.edit().putBoolean("has_root_access", bool).apply()
.edit().putBoolean("has_root_access", bool).apply()
} }
val isCrashReportingEnabled: Boolean val isCrashReportingEnabled: Boolean
get() = SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm")!! get() = SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING) "pref_crash_reporting",
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING
)
val bootSharedPreferences: SharedPreferences? val bootSharedPreferences: SharedPreferences?
get() = getSharedPreferences("mmm_boot") get() = getSharedPreferences("mmm_boot")
@ -918,8 +687,10 @@ class MainApplication : FoxApplication(), Configuration.Provider {
@JvmStatic @JvmStatic
fun isMatomoAllowed(): Boolean { fun isMatomoAllowed(): Boolean {
return getSharedPreferences("mmm")!! return getSharedPreferences("mmm")!!.getBoolean(
.getBoolean("pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS) "pref_analytics_enabled",
BuildConfig.DEFAULT_ENABLE_ANALYTICS
)
} }
} }
} }

@ -21,8 +21,11 @@ import androidx.fragment.app.FragmentActivity
import androidx.room.Room import androidx.room.Room
import com.fox2code.foxcompat.app.FoxActivity import com.fox2code.foxcompat.app.FoxActivity
import com.fox2code.mmm.databinding.ActivitySetupBinding import com.fox2code.mmm.databinding.ActivitySetupBinding
import com.fox2code.mmm.repo.RepoManager
import com.fox2code.mmm.utils.IntentHelper import com.fox2code.mmm.utils.IntentHelper
import com.fox2code.mmm.utils.room.ModuleListCache
import com.fox2code.mmm.utils.room.ModuleListCacheDatabase import com.fox2code.mmm.utils.room.ModuleListCacheDatabase
import com.fox2code.mmm.utils.room.ReposList
import com.fox2code.mmm.utils.room.ReposListDatabase import com.fox2code.mmm.utils.room.ReposListDatabase
import com.fox2code.rosettax.LanguageActivity import com.fox2code.rosettax.LanguageActivity
import com.fox2code.rosettax.LanguageSwitcher import com.fox2code.rosettax.LanguageSwitcher
@ -227,10 +230,11 @@ class SetupActivity : FoxActivity(), LanguageActivity {
val db = Room.databaseBuilder( val db = Room.databaseBuilder(
applicationContext, applicationContext,
ReposListDatabase::class.java, "ReposList.db" ReposListDatabase::class.java, "ReposList.db"
).build() ).allowMainThreadQueries().build()
val androidacyRepoRoom = andRepoView.isChecked val androidacyRepoRoom = andRepoView.isChecked
val magiskAltRepoRoom = magiskAltRepoView.isChecked val magiskAltRepoRoom = magiskAltRepoView.isChecked
val reposListDao = db.reposListDao() val reposListDao = db.reposListDao()
Timber.d(reposListDao.getAll().toString())
val androidacyRepoRoomObj = reposListDao.getById("androidacy_repo") val androidacyRepoRoomObj = reposListDao.getById("androidacy_repo")
val magiskAltRepoRoomObj = reposListDao.getById("magisk_alt_repo") val magiskAltRepoRoomObj = reposListDao.getById("magisk_alt_repo")
reposListDao.setEnabled(androidacyRepoRoomObj.id, androidacyRepoRoom) reposListDao.setEnabled(androidacyRepoRoomObj.id, androidacyRepoRoom)
@ -239,12 +243,6 @@ class SetupActivity : FoxActivity(), LanguageActivity {
editor.putString("last_shown_setup", "v3") editor.putString("last_shown_setup", "v3")
// Commit the changes // Commit the changes
editor.commit() editor.commit()
// sleep to allow the realm transaction to finish
try {
Thread.sleep(250)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
// Log the changes // Log the changes
Timber.d("Setup finished. Preferences: %s", prefs.all) Timber.d("Setup finished. Preferences: %s", prefs.all)
Timber.d("Androidacy repo: %s", androidacyRepoRoom) Timber.d("Androidacy repo: %s", androidacyRepoRoom)
@ -329,19 +327,112 @@ class SetupActivity : FoxActivity(), LanguageActivity {
// creates the room database // creates the room database
private fun createDatabases() { private fun createDatabases() {
val thread = Thread {
Timber.d("Creating databases")
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val appContext = MainApplication.INSTANCE!!.applicationContext val appContext = MainApplication.INSTANCE!!.applicationContext
Room.databaseBuilder(appContext, ReposListDatabase::class.java, "reposlist.db") val db = Room.databaseBuilder(appContext, ReposListDatabase::class.java, "ReposList.db")
.createFromAsset("assets/reposlist.db") .fallbackToDestructiveMigration().build()
.fallbackToDestructiveMigration()
.build()
// same for modulelistcache // same for modulelistcache
Room.databaseBuilder(appContext, ModuleListCacheDatabase::class.java, "modulelistcache.db") val db2 = Room.databaseBuilder(
.createFromAsset("assets/modulelistcache.db") appContext,
ModuleListCacheDatabase::class.java,
"ModuleListCache.db"
)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .allowMainThreadQueries().build()
val reposListDao = db.reposListDao()
val moduleListCacheDao = db2.moduleListCacheDao()
// create the androidacy repo
val androidacyRepo = ReposList(
"androidacy_repo",
RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT,
true,
"https://www.androidacy.com/membership-join/",
"https://t.me/androidacy",
"https://www.androidacy.com/module-repository-applications//",
0,
"Androidacy Modules Repo",
"https://www.androidacy.com/"
)
// create the magisk alt repo
val magiskAltRepo = ReposList(
"magisk_alt_repo",
RepoManager.MAGISK_ALT_REPO_JSDELIVR,
false,
"",
"",
RepoManager.MAGISK_ALT_REPO_HOMEPAGE + "/submission/",
0,
"Magisk Alt Modules Repo",
RepoManager.MAGISK_ALT_REPO_HOMEPAGE
)
// insert the repos into the database
reposListDao.insert(androidacyRepo)
reposListDao.insert(magiskAltRepo)
// create the modulelistcache
val moduleListCache = ModuleListCache(
codename = "androidacy_repo",
version = "",
versionCode = 0,
author = "",
description = "",
minApi = 0,
maxApi = 99999,
minMagisk = 0,
needRamdisk = false,
support = "",
donate = "",
config = "",
changeBoot = false,
mmtReborn = false,
repoId = "androidacy_repo",
lastUpdate = 0,
name = "",
safe = false,
stats = 0,
)
// insert the modulelistcache into the database
moduleListCacheDao.insert(moduleListCache)
// now make sure reposlist is updated with 2 entries and modulelistcache is updated with 1 entry
val reposList = reposListDao.getAll()
val moduleListCacheList = moduleListCacheDao.getAll()
// make sure reposlist is updated with 2 entries
if (reposList.size != 2) {
Timber.e("ReposList is not updated with 2 entries")
// show a toast
runOnUiThread {
Toast.makeText(
this,
R.string.error_creating_repos_database,
Toast.LENGTH_LONG
).show()
}
} else {
Timber.d("ReposList is updated with 2 entries")
}
// make sure modulelistcache is updated with 1 entry
if (moduleListCacheList.size != 1) {
Timber.e("ModuleListCache is not updated with 1 entry")
// show a toast
runOnUiThread {
Toast.makeText(
this,
R.string.error_creating_modulelistcache_database,
Toast.LENGTH_LONG
).show()
}
} else {
Timber.d("ModuleListCache is updated with 1 entry")
}
// close the databases
db.close()
db2.close()
Timber.d("Databases created in %s ms", System.currentTimeMillis() - startTime) Timber.d("Databases created in %s ms", System.currentTimeMillis() - startTime)
} }
thread.start()
}
private fun createFiles() { private fun createFiles() {
// use cookiemanager to create the cookie database // use cookiemanager to create the cookie database

@ -66,12 +66,11 @@ class ModuleManager private constructor() : SyncManager() {
var moduleInfo = moduleInfos[module] var moduleInfo = moduleInfos[module]
// next, merge the module info with a record from ModuleListCache room db if it exists // next, merge the module info with a record from ModuleListCache room db if it exists
// initialize modulelistcache db // initialize modulelistcache db
// DO NOT USE REALM ANYMORE
val db = Room.databaseBuilder( val db = Room.databaseBuilder(
MainApplication.INSTANCE!!, MainApplication.INSTANCE!!,
ModuleListCacheDatabase::class.java, ModuleListCacheDatabase::class.java,
"ModuleListCache" "ModuleListCache"
).build() ).allowMainThreadQueries().build()
// get module info from cache // get module info from cache
val moduleListCacheDao: ModuleListCacheDao = db.moduleListCacheDao() val moduleListCacheDao: ModuleListCacheDao = db.moduleListCacheDao()
Timber.d("Found cache for %s", module) Timber.d("Found cache for %s", module)

@ -1,376 +1,430 @@
/* /*
* 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. * 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.module
package com.fox2code.mmm.module; import android.annotation.SuppressLint
import android.content.DialogInterface
import android.net.Uri
import android.text.Spanned
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes
import com.fox2code.foxcompat.app.FoxActivity
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.mmm.MainApplication.Companion.INSTANCE
import com.fox2code.mmm.MainApplication.Companion.isShowcaseMode
import com.fox2code.mmm.R
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.isAndroidacyLink
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath
import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.manager.ModuleManager.Companion.instance
import com.fox2code.mmm.utils.ExternalHelper
import com.fox2code.mmm.utils.IntentHelper.Companion.openConfig
import com.fox2code.mmm.utils.IntentHelper.Companion.openCustomTab
import com.fox2code.mmm.utils.IntentHelper.Companion.openInstaller
import com.fox2code.mmm.utils.IntentHelper.Companion.openMarkdown
import com.fox2code.mmm.utils.IntentHelper.Companion.openUrl
import com.fox2code.mmm.utils.IntentHelper.Companion.openUrlAndroidacy
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.noties.markwon.Markwon
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.util.Objects
import android.annotation.SuppressLint; @Suppress("SENSELESS_COMPARISON")
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.Spanned;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.appcompat.app.AlertDialog;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.chip.Chip;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.matomo.sdk.extra.TrackHelper;
import java.util.Objects;
import io.noties.markwon.Markwon;
import timber.log.Timber;
@SuppressWarnings("ReplaceNullCheck")
@SuppressLint("UseCompatLoadingForDrawables") @SuppressLint("UseCompatLoadingForDrawables")
public enum ActionButtonType { enum class ActionButtonType {
INFO() { INFO {
@Override override fun update(button: Chip, moduleHolder: ModuleHolder) {
public void update(Chip button, ModuleHolder moduleHolder) { button.chipIcon = button.context.getDrawable(R.drawable.ic_baseline_info_24)
button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_info_24)); button.setText(R.string.description)
button.setText(R.string.description);
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { val name: String? = if (moduleHolder.moduleInfo != null) {
String name; moduleHolder.moduleInfo!!.name
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("view_notes", name).with(INSTANCE!!.getTracker())
String notesUrl = moduleHolder.repoModule.notesUrl; val notesUrl = moduleHolder.repoModule?.notesUrl
if (AndroidacyUtil.Companion.isAndroidacyLink(notesUrl)) { if (isAndroidacyLink(notesUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig()); openUrlAndroidacy(
button.context,
notesUrl,
false,
moduleHolder.repoModule?.moduleInfo?.name,
moduleHolder.mainModuleConfig
)
} else { } else {
IntentHelper.openMarkdown(button.getContext(), notesUrl, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig(), moduleHolder.repoModule.moduleInfo.changeBoot, moduleHolder.repoModule.moduleInfo.needRamdisk, moduleHolder.repoModule.moduleInfo.minMagisk, moduleHolder.repoModule.moduleInfo.minApi, moduleHolder.repoModule.moduleInfo.maxApi); openMarkdown(
button.context,
notesUrl,
moduleHolder.repoModule?.moduleInfo?.name,
moduleHolder.mainModuleConfig,
moduleHolder.repoModule?.moduleInfo?.changeBoot,
moduleHolder.repoModule?.moduleInfo?.needRamdisk,
moduleHolder.repoModule?.moduleInfo?.minMagisk ?: 0,
moduleHolder.repoModule?.moduleInfo?.minApi ?: 0,
moduleHolder.repoModule?.moduleInfo?.maxApi ?: 9999
)
} }
} }
@Override override fun doActionLong(button: Chip, moduleHolder: ModuleHolder): Boolean {
public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { val context = button.context
Context context = button.getContext(); Toast.makeText(
Toast.makeText(context, context.getString(R.string.module_id_prefix) + moduleHolder.moduleId, Toast.LENGTH_SHORT).show(); context,
return true; context.getString(R.string.module_id_prefix) + moduleHolder.moduleId,
} Toast.LENGTH_SHORT
}, UPDATE_INSTALL() { ).show()
@Override return true
public void update(Chip button, ModuleHolder moduleHolder) { }
int icon; },
UPDATE_INSTALL {
override fun update(button: Chip, moduleHolder: ModuleHolder) {
val icon: Int
if (moduleHolder.hasUpdate()) { if (moduleHolder.hasUpdate()) {
icon = R.drawable.ic_baseline_update_24; icon = R.drawable.ic_baseline_update_24
button.setText(R.string.update); button.setText(R.string.update)
} else if (moduleHolder.moduleInfo != null) { } else if (moduleHolder.moduleInfo != null) {
icon = R.drawable.ic_baseline_refresh_24; icon = R.drawable.ic_baseline_refresh_24
button.setText(R.string.reinstall); button.setText(R.string.reinstall)
} else { } else {
icon = R.drawable.ic_baseline_system_update_24; icon = R.drawable.ic_baseline_system_update_24
button.setText(R.string.install); button.setText(R.string.install)
} }
button.setChipIcon(button.getContext().getDrawable(icon)); button.chipIcon = button.context.getDrawable(icon)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { // if mainmoduleinfo is null, we are in repo mode
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); val moduleInfo: ModuleInfo = if (moduleHolder.mainModuleInfo != null) {
if (moduleInfo == null) return; moduleHolder.mainModuleInfo
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo ?: return
} }
TrackHelper.track().event("view_update_install", name).with(MainApplication.getINSTANCE().getTracker()); val name: String? = if (moduleHolder.moduleInfo != null) {
moduleHolder.moduleInfo!!.name
} else {
moduleHolder.repoModule?.moduleInfo?.name
}
TrackHelper.track().event("view_update_install", name).with(INSTANCE!!.getTracker())
// if icon is reinstall, we need to uninstall first - warn the user but don't proceed // if icon is reinstall, we need to uninstall first - warn the user but don't proceed
if (moduleHolder.moduleInfo != null) { if (moduleHolder.moduleInfo != null) {
// get icon of the button // get icon of the button
Drawable icon = button.getChipIcon(); val icon = button.chipIcon
if (icon != null && icon.getConstantState() != null) { if (icon != null && icon.constantState != null) {
Drawable reinstallIcon = button.getContext().getDrawable(R.drawable.ic_baseline_refresh_24); val reinstallIcon =
if (reinstallIcon != null && reinstallIcon.getConstantState() != null) { button.context.getDrawable(R.drawable.ic_baseline_refresh_24)
if (icon.getConstantState().equals(reinstallIcon.getConstantState())) { if (reinstallIcon != null && reinstallIcon.constantState != null) {
new MaterialAlertDialogBuilder(button.getContext()) if (icon.constantState == reinstallIcon.constantState) {
MaterialAlertDialogBuilder(button.context)
.setTitle(R.string.reinstall) .setTitle(R.string.reinstall)
.setMessage(R.string.reinstall_warning) .setMessage(R.string.reinstall_warning)
.setPositiveButton(R.string.reinstall, null) .setPositiveButton(R.string.reinstall, null)
.show(); .show()
return; return
} }
} }
} }
} }
String updateZipUrl = moduleHolder.getUpdateZipUrl(); val updateZipUrl = moduleHolder.updateZipUrl ?: return
if (updateZipUrl == null) return;
// Androidacy manage the selection between download and install // Androidacy manage the selection between download and install
if (AndroidacyUtil.Companion.isAndroidacyLink(updateZipUrl)) { if (isAndroidacyLink(updateZipUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), updateZipUrl, true, moduleInfo.name, moduleInfo.config); openUrlAndroidacy(
return; button.context,
} updateZipUrl,
boolean hasRoot = InstallerInitializer.peekMagiskPath() != null && !MainApplication.Companion.isShowcaseMode(); true,
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(button.getContext()); moduleInfo.name,
builder.setTitle(moduleInfo.name).setCancelable(true).setIcon(R.drawable.ic_baseline_extension_24); moduleInfo.config
CharSequence desc = moduleInfo.description; )
Markwon markwon = null; return
LocalModuleInfo localModuleInfo = moduleHolder.moduleInfo; }
if (localModuleInfo != null && !localModuleInfo.updateChangeLog.isEmpty()) { val hasRoot = peekMagiskPath() != null && !isShowcaseMode
markwon = MainApplication.getINSTANCE().getMarkwon(); val builder = MaterialAlertDialogBuilder(button.context)
builder.setTitle(moduleInfo.name).setCancelable(true)
.setIcon(R.drawable.ic_baseline_extension_24)
var desc: CharSequence? = moduleInfo.description
var markwon: Markwon? = null
val localModuleInfo = moduleHolder.moduleInfo
if (localModuleInfo != null && localModuleInfo.updateChangeLog.isNotEmpty()) {
markwon = INSTANCE!!.getMarkwon()
// Re-render each time in cse of config changes // Re-render each time in cse of config changes
desc = markwon.toMarkdown(localModuleInfo.updateChangeLog); desc = markwon!!.toMarkdown(localModuleInfo.updateChangeLog)
} }
if (desc.isNullOrEmpty()) {
if (desc == null || desc.length() == 0) { builder.setMessage(R.string.no_desc_found)
builder.setMessage(R.string.no_desc_found);
} else { } else {
builder.setMessage(desc); builder.setMessage(desc)
} }
Timber.i("URL: %s", updateZipUrl); Timber.i("URL: %s", updateZipUrl)
builder.setNegativeButton(R.string.download_module, (x, y) -> IntentHelper.openCustomTab(button.getContext(), updateZipUrl)); builder.setNegativeButton(R.string.download_module) { _: DialogInterface?, _: Int ->
if (hasRoot) { openCustomTab(
builder.setPositiveButton(moduleHolder.hasUpdate() ? R.string.update_module : R.string.install_module, (x, y) -> { button.context,
String updateZipChecksum = moduleHolder.getUpdateZipChecksum(); updateZipUrl
IntentHelper.openInstaller(button.getContext(), updateZipUrl, moduleInfo.name, moduleInfo.config, updateZipChecksum, moduleInfo.mmtReborn); )
});
} }
ExternalHelper.INSTANCE.injectButton(builder, () -> Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo()); if (hasRoot) {
int dim5dp = FoxDisplay.dpToPixel(5); builder.setPositiveButton(if (moduleHolder.hasUpdate()) R.string.update_module else R.string.install_module) { _: DialogInterface?, _: Int ->
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp); val updateZipChecksum = moduleHolder.updateZipChecksum
AlertDialog alertDialog = builder.show(); openInstaller(
for (int i = -3; i < 0; i++) { button.context,
Button alertButton = alertDialog.getButton(i); updateZipUrl,
if (alertButton != null && alertButton.getPaddingStart() > dim5dp) { moduleInfo.name,
alertButton.setPadding(dim5dp, dim5dp, dim5dp, dim5dp); moduleInfo.config,
updateZipChecksum,
moduleInfo.mmtReborn
)
}
}
ExternalHelper.INSTANCE.injectButton(
builder,
{ Uri.parse(updateZipUrl) },
moduleHolder.updateZipRepo
)
val dim5dp = FoxDisplay.dpToPixel(5f)
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp)
val alertDialog = builder.show()
for (i in -3..-1) {
val alertButton = alertDialog.getButton(i)
if (alertButton != null && alertButton.paddingStart > dim5dp) {
alertButton.setPadding(dim5dp, dim5dp, dim5dp, dim5dp)
} }
} }
if (markwon != null) { if (markwon != null) {
TextView messageView = Objects.requireNonNull(alertDialog.getWindow()).findViewById(android.R.id.message); val messageView = alertDialog.window!!.findViewById<TextView>(android.R.id.message)
markwon.setParsedMarkdown(messageView, (Spanned) desc); markwon.setParsedMarkdown(messageView, (desc as Spanned?)!!)
} }
} }
}, UNINSTALL() { },
@Override UNINSTALL {
public void update(Chip button, ModuleHolder moduleHolder) { override fun update(button: Chip, moduleHolder: ModuleHolder) {
int icon = moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING) ? R.drawable.ic_baseline_delete_outline_24 : (!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) || moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) ? R.drawable.ic_baseline_delete_24 : R.drawable.ic_baseline_delete_forever_24; val icon =
button.setChipIcon(button.getContext().getDrawable(icon)); if (moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING)) R.drawable.ic_baseline_delete_outline_24 else if (!moduleHolder.hasFlag(
button.setText(R.string.uninstall); ModuleInfo.FLAG_MODULE_UPDATING
) || moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)
) R.drawable.ic_baseline_delete_24 else R.drawable.ic_baseline_delete_forever_24
button.chipIcon = button.context.getDrawable(icon)
button.setText(R.string.uninstall)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE or ModuleInfo.FLAG_MODULE_UNINSTALLING) && moduleHolder.hasFlag(
if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UNINSTALLING) && moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) { ModuleInfo.FLAG_MODULE_UPDATING
doActionLong(button, moduleHolder); )
return; ) {
} doActionLong(button, moduleHolder)
String name; return
if (moduleHolder.moduleInfo != null) { }
name = moduleHolder.moduleInfo.name; val name: String? = if (moduleHolder.moduleInfo != null) {
moduleHolder.moduleInfo!!.name
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("uninstall_module", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("uninstall_module", name).with(INSTANCE!!.getTracker())
Timber.i(Integer.toHexString(moduleHolder.moduleInfo.flags)); Timber.i(Integer.toHexString(moduleHolder.moduleInfo?.flags ?: 0))
if (!Objects.requireNonNull(ModuleManager.getInstance()).setUninstallState(moduleHolder.moduleInfo, !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) { if (!instance!!.setUninstallState(
Timber.e("Failed to switch uninstalled state!"); moduleHolder.moduleInfo!!, !moduleHolder.hasFlag(
ModuleInfo.FLAG_MODULE_UNINSTALLING
)
)
) {
Timber.e("Failed to switch uninstalled state!")
} }
update(button, moduleHolder); update(button, moduleHolder)
} }
@Override override fun doActionLong(button: Chip, moduleHolder: ModuleHolder): Boolean {
public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { if (moduleHolder.moduleInfo == null) {
return false
}
// Actually a module having mount is the only issue when deleting module // Actually a module having mount is the only issue when deleting module
if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) if (moduleHolder.moduleInfo!!.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) return false // We can't trust active flag on first boot
return false; // We can't trust active flag on first boot val builder = MaterialAlertDialogBuilder(button.context)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(button.getContext()); builder.setTitle(R.string.master_delete)
builder.setTitle(R.string.master_delete); builder.setPositiveButton(R.string.master_delete_yes) { _: DialogInterface?, _: Int ->
builder.setPositiveButton(R.string.master_delete_yes, (dialog, which) -> { val moduleId = moduleHolder.moduleInfo!!.id
String moduleId = moduleHolder.moduleInfo.id; if (!instance!!.masterClear(moduleHolder.moduleInfo!!)) {
if (!Objects.requireNonNull(ModuleManager.getInstance()).masterClear(moduleHolder.moduleInfo)) { Toast.makeText(button.context, R.string.master_delete_fail, Toast.LENGTH_SHORT)
Toast.makeText(button.getContext(), R.string.master_delete_fail, Toast.LENGTH_SHORT).show(); .show()
} else { } else {
moduleHolder.moduleInfo = null; moduleHolder.moduleInfo = null
FoxActivity.getFoxActivity(button).refreshUI(); FoxActivity.getFoxActivity(button).refreshUI()
Timber.e("Cleared: %s", moduleId); Timber.e("Cleared: %s", moduleId)
} }
}); }
builder.setNegativeButton(R.string.master_delete_no, (v, i) -> { builder.setNegativeButton(R.string.master_delete_no) { _: DialogInterface?, _: Int -> }
}); builder.create()
builder.create(); builder.show()
builder.show(); return true
return true; }
} },
}, CONFIG() { CONFIG {
@Override override fun update(button: Chip, moduleHolder: ModuleHolder) {
public void update(Chip button, ModuleHolder moduleHolder) { button.chipIcon =
button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_app_settings_alt_24)); button.context.getDrawable(R.drawable.ic_baseline_app_settings_alt_24)
button.setText(R.string.config); button.setText(R.string.config)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { if (moduleHolder.moduleInfo == null) {
String config = moduleHolder.getMainModuleConfig(); return
if (config == null) return; }
val config = moduleHolder.mainModuleConfig ?: return
String name; val name: String? = if (moduleHolder.moduleInfo != null) {
if (moduleHolder.moduleInfo != null) { moduleHolder.moduleInfo!!.name
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("config_module", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("config_module", name).with(INSTANCE!!.getTracker())
if (AndroidacyUtil.Companion.isAndroidacyLink(config)) { if (isAndroidacyLink(config)) {
IntentHelper.openUrlAndroidacy(button.getContext(), config, true); openUrlAndroidacy(button.context, config, true)
} else { } else {
IntentHelper.openConfig(button.getContext(), config); openConfig(button.context, config)
} }
} }
}, SUPPORT() { },
@Override SUPPORT {
public void update(Chip button, ModuleHolder moduleHolder) { override fun update(button: Chip, moduleHolder: ModuleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); val moduleInfo = moduleHolder.mainModuleInfo
button.setChipIcon(button.getContext().getDrawable(supportIconForUrl(moduleInfo.support))); button.chipIcon = button.context.getDrawable(
button.setText(R.string.support); supportIconForUrl(moduleInfo.support)
)
button.setText(R.string.support)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { val name: String? = if (moduleHolder.moduleInfo != null) {
moduleHolder.moduleInfo!!.name
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("support_module", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("support_module", name).with(INSTANCE!!.getTracker())
IntentHelper.Companion.openUrl(button.getContext(), Objects.requireNonNull(moduleHolder.getMainModuleInfo().support)); openUrl(button.context, Objects.requireNonNull(moduleHolder.mainModuleInfo.support))
} }
}, DONATE() { },
@Override DONATE {
public void update(Chip button, ModuleHolder moduleHolder) { override fun update(button: Chip, moduleHolder: ModuleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); val moduleInfo = moduleHolder.mainModuleInfo
button.setChipIcon(button.getContext().getDrawable(donateIconForUrl(moduleInfo.donate))); button.chipIcon = button.context.getDrawable(
button.setText(R.string.donate); donateIconForUrl(moduleInfo.donate)
)
button.setText(R.string.donate)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { val name: String? = if (moduleHolder.moduleInfo != null) {
String name; moduleHolder.moduleInfo!!.name
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("donate_module", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("donate_module", name).with(INSTANCE!!.getTracker())
IntentHelper.Companion.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate); openUrl(button.context, moduleHolder.mainModuleInfo.donate)
} }
}, WARNING() { },
@Override WARNING {
public void update(Chip button, ModuleHolder moduleHolder) { override fun update(button: Chip, moduleHolder: ModuleHolder) {
button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_warning_24)); button.chipIcon = button.context.getDrawable(R.drawable.ic_baseline_warning_24)
button.setText(R.string.warning); button.setText(R.string.warning)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { val name: String? = if (moduleHolder.moduleInfo != null) {
String name; moduleHolder.moduleInfo!!.name
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("warning_module", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("warning_module", name).with(INSTANCE!!.getTracker())
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.warning).setMessage(R.string.warning_message).setPositiveButton(R.string.understand, (v, i) -> { MaterialAlertDialogBuilder(button.context).setTitle(R.string.warning)
}).create().show(); .setMessage(R.string.warning_message).setPositiveButton(
R.string.understand
) { _: DialogInterface?, _: Int -> }
.create().show()
} }
}, SAFE() { },
SAFE {
// SAFE is for modules that the api says are clean. only supported by androidacy currently // SAFE is for modules that the api says are clean. only supported by androidacy currently
@Override override fun update(button: Chip, moduleHolder: ModuleHolder) {
public void update(Chip button, ModuleHolder moduleHolder) { button.chipIcon =
button.setChipIcon(button.getContext().getDrawable(R.drawable.baseline_verified_user_24)); button.context.getDrawable(R.drawable.baseline_verified_user_24)
button.setText(R.string.safe); button.setText(R.string.safe)
} }
@Override override fun doAction(button: Chip, moduleHolder: ModuleHolder) {
public void doAction(Chip button, ModuleHolder moduleHolder) { val name: String? = if (moduleHolder.moduleInfo != null) {
String name; moduleHolder.moduleInfo!!.name
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else { } else {
name = moduleHolder.repoModule.moduleInfo.name; moduleHolder.repoModule?.moduleInfo?.name
} }
TrackHelper.track().event("safe_module", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("safe_module", name).with(INSTANCE!!.getTracker())
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.safe_module).setMessage(R.string.safe_message).setPositiveButton(R.string.understand, (v, i) -> { MaterialAlertDialogBuilder(button.context).setTitle(R.string.safe_module)
}).create().show(); .setMessage(R.string.safe_message).setPositiveButton(
R.string.understand
) { _: DialogInterface?, _: Int -> }
.create().show()
} }
}; };
@DrawableRes @DrawableRes
private final int iconId; private val iconId: Int
constructor() {
iconId = 0
}
@Suppress("unused")
constructor(iconId: Int) {
this.iconId = iconId
}
ActionButtonType() { open fun update(button: Chip, moduleHolder: ModuleHolder) {
this.iconId = 0; button.chipIcon = button.context.getDrawable(iconId)
} }
@SuppressWarnings("unused") abstract fun doAction(button: Chip, moduleHolder: ModuleHolder)
ActionButtonType(int iconId) { open fun doActionLong(button: Chip, moduleHolder: ModuleHolder): Boolean {
this.iconId = iconId; return false
} }
companion object {
@JvmStatic
@DrawableRes @DrawableRes
public static int supportIconForUrl(String url) { fun supportIconForUrl(url: String?): Int {
int icon = R.drawable.ic_baseline_support_24; var icon = R.drawable.ic_baseline_support_24
if (url == null) { if (url == null) {
return icon; return icon
} else if (url.startsWith("https://t.me/")) { } else if (url.startsWith("https://t.me/")) {
icon = R.drawable.ic_baseline_telegram_24; icon = R.drawable.ic_baseline_telegram_24
} else if (url.startsWith("https://discord.gg/") || url.startsWith("https://discord.com/invite/")) { } else if (url.startsWith("https://discord.gg/") || url.startsWith("https://discord.com/invite/")) {
icon = R.drawable.ic_baseline_discord_24; icon = R.drawable.ic_baseline_discord_24
} else if (url.startsWith("https://github.com/")) { } else if (url.startsWith("https://github.com/")) {
icon = R.drawable.ic_github; icon = R.drawable.ic_github
} else if (url.startsWith("https://gitlab.com/")) { } else if (url.startsWith("https://gitlab.com/")) {
icon = R.drawable.ic_gitlab; icon = R.drawable.ic_gitlab
} else if (url.startsWith("https://forum.xda-developers.com/")) { } else if (url.startsWith("https://forum.xda-developers.com/")) {
icon = R.drawable.ic_xda; icon = R.drawable.ic_xda
} }
return icon; return icon
} }
@JvmStatic
@DrawableRes @DrawableRes
public static int donateIconForUrl(String url) { fun donateIconForUrl(url: String?): Int {
int icon = R.drawable.ic_baseline_monetization_on_24; var icon = R.drawable.ic_baseline_monetization_on_24
if (url == null) { if (url == null) {
return icon; return icon
} else if (url.startsWith("https://www.paypal.me/") || url.startsWith("https://www.paypal.com/paypalme/") || url.startsWith("https://www.paypal.com/donate/")) { } else if (url.startsWith("https://www.paypal.me/") || url.startsWith("https://www.paypal.com/paypalme/") || url.startsWith(
icon = R.drawable.ic_baseline_paypal_24; "https://www.paypal.com/donate/"
)
) {
icon = R.drawable.ic_baseline_paypal_24
} else if (url.startsWith("https://patreon.com/") || url.startsWith("https://www.patreon.com/")) { } else if (url.startsWith("https://patreon.com/") || url.startsWith("https://www.patreon.com/")) {
icon = R.drawable.ic_patreon; icon = R.drawable.ic_patreon
} }
return icon; return icon
} }
public void update(Chip button, ModuleHolder moduleHolder) {
button.setChipIcon(button.getContext().getDrawable(this.iconId));
}
public abstract void doAction(Chip button, ModuleHolder moduleHolder);
public boolean doActionLong(Chip button, ModuleHolder moduleHolder) {
return false;
} }
} }

@ -1,374 +1,420 @@
/* /*
* 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. * 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.module
package com.fox2code.mmm.module;
import android.content.Context
import android.content.Context; import android.content.pm.PackageManager
import android.content.pm.PackageManager; import android.view.View
import android.view.View; import androidx.annotation.StringRes
import com.fox2code.mmm.MainApplication.Companion.INSTANCE
import androidx.annotation.NonNull; import com.fox2code.mmm.MainApplication.Companion.formatTime
import androidx.annotation.StringRes; import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences
import com.fox2code.mmm.MainApplication.Companion.isDisableLowQualityModuleFilter
import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.NotificationType
import com.fox2code.mmm.NotificationType; import com.fox2code.mmm.R
import com.fox2code.mmm.R; import com.fox2code.mmm.XHooks.Companion.checkConfigTargetExists
import com.fox2code.mmm.XHooks; import com.fox2code.mmm.manager.LocalModuleInfo
import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.repo.RepoModule
import com.fox2code.mmm.repo.RepoModule; import com.fox2code.mmm.utils.IntentHelper.Companion.getPackageOfConfig
import com.fox2code.mmm.utils.IntentHelper; import com.fox2code.mmm.utils.io.PropUtils.Companion.isLowQualityModule
import com.fox2code.mmm.utils.io.PropUtils; import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
import com.fox2code.mmm.utils.io.net.Http; import timber.log.Timber
import java.util.Objects
import java.util.Comparator;
import java.util.HashSet; @Suppress("unused", "KotlinConstantConditions")
import java.util.List; class ModuleHolder : Comparable<ModuleHolder?> {
import java.util.Locale; val moduleId: String
import java.util.Objects; val notificationType: NotificationType?
import java.util.Set; val separator: Type?
var footerPx: Int
import timber.log.Timber; var onClickListener: View.OnClickListener? = null
var moduleInfo: LocalModuleInfo? = null
public final class ModuleHolder implements Comparable<ModuleHolder> { var repoModule: RepoModule? = null
var filterLevel = 0
public final String moduleId;
public final NotificationType notificationType; constructor(moduleId: String) {
public final Type separator; this.moduleId = Objects.requireNonNull(moduleId)
public int footerPx; notificationType = null
public View.OnClickListener onClickListener; separator = null
footerPx = -1
public LocalModuleInfo moduleInfo; }
public RepoModule repoModule;
public int filterLevel; constructor(notificationType: NotificationType) {
moduleId = ""
public ModuleHolder(String moduleId) { this.notificationType = Objects.requireNonNull(notificationType)
this.moduleId = Objects.requireNonNull(moduleId); separator = null
this.notificationType = null; footerPx = -1
this.separator = null; }
this.footerPx = -1;
} constructor(separator: Type?) {
moduleId = ""
public ModuleHolder(NotificationType notificationType) { notificationType = null
this.moduleId = ""; this.separator = separator
this.notificationType = Objects.requireNonNull(notificationType); footerPx = -1
this.separator = null; }
this.footerPx = -1;
} @Suppress("unused")
constructor(footerPx: Int, header: Boolean) {
public ModuleHolder(Type separator) { moduleId = ""
this.moduleId = ""; notificationType = null
this.notificationType = null; separator = null
this.separator = separator; this.footerPx = footerPx
this.footerPx = -1; filterLevel = if (header) 1 else 0
} }
@SuppressWarnings("unused") val isModuleHolder: Boolean
public ModuleHolder(int footerPx, boolean header) { get() = notificationType == null && separator == null && footerPx == -1
this.moduleId = ""; val mainModuleInfo: ModuleInfo
this.notificationType = null; get() = if (repoModule != null && (moduleInfo == null || moduleInfo!!.versionCode < repoModule!!.moduleInfo.versionCode)) repoModule!!.moduleInfo else moduleInfo!!
this.separator = null; val updateZipUrl: String?
this.footerPx = footerPx; get() = if (moduleInfo == null || repoModule != null && moduleInfo!!.updateVersionCode < repoModule!!.moduleInfo.versionCode) repoModule!!.zipUrl else moduleInfo!!.updateZipUrl
this.filterLevel = header ? 1 : 0; val updateZipRepo: String?
} get() = if (moduleInfo == null || repoModule != null && moduleInfo!!.updateVersionCode < repoModule!!.moduleInfo.versionCode) repoModule!!.repoData.preferenceId else "update_json"
val updateZipChecksum: String?
public boolean isModuleHolder() { get() = if (moduleInfo == null || repoModule != null && moduleInfo!!.updateVersionCode < repoModule!!.moduleInfo.versionCode) repoModule!!.checksum else moduleInfo!!.updateChecksum
return this.notificationType == null && this.separator == null && this.footerPx == -1; val mainModuleName: String?
} get() {
val moduleInfo = mainModuleInfo
public ModuleInfo getMainModuleInfo() { if (moduleInfo.name == null) throw Error("Error for " + type.name + " id " + moduleId)
return this.repoModule != null && (this.moduleInfo == null || this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.moduleInfo : this.moduleInfo; return moduleInfo.name
} }
val mainModuleNameLowercase: String
public String getUpdateZipUrl() { get() = mainModuleName!!.lowercase()
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.zipUrl : this.moduleInfo.updateZipUrl; val mainModuleConfig: String?
} get() {
if (moduleInfo == null) return null
public String getUpdateZipRepo() { var config = moduleInfo!!.config
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.repoData.preferenceId : "update_json"; if (config == null && repoModule != null) {
} config = repoModule!!.moduleInfo.config
}
public String getUpdateZipChecksum() { return config
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.checksum : this.moduleInfo.updateChecksum; }
} val updateTimeText: String
get() {
public String getMainModuleName() { if (repoModule == null) return ""
ModuleInfo moduleInfo = this.getMainModuleInfo(); val timeStamp = repoModule!!.lastUpdated
if (moduleInfo == null || moduleInfo.name == null) return if (timeStamp <= 0) "" else formatTime(timeStamp)
throw new Error("Error for " + this.getType().name() + " id " + this.moduleId); }
return moduleInfo.name; val repoName: String?
} get() = if (repoModule == null) "" else repoModule!!.repoName
public String getMainModuleNameLowercase() { fun hasFlag(flag: Int): Boolean {
return this.getMainModuleName().toLowerCase(Locale.ROOT); return moduleInfo != null && moduleInfo!!.hasFlag(flag)
} }
public String getMainModuleConfig() { val type: Type
if (this.moduleInfo == null) return null; get() = if (footerPx != -1) {
String config = this.moduleInfo.config; Timber.i("Module %s is footer", moduleId)
if (config == null && this.repoModule != null) { Type.FOOTER
config = this.repoModule.moduleInfo.config; } else if (separator != null) {
} Timber.i("Module %s is separator", moduleId)
return config; Type.SEPARATOR
} } else if (notificationType != null) {
Timber.i("Module %s is notification", moduleId)
public String getUpdateTimeText() { Type.NOTIFICATION
if (this.repoModule == null) return ""; } else if (moduleInfo == null) {
long timeStamp = this.repoModule.lastUpdated; Type.INSTALLABLE
return timeStamp <= 0 ? "" : MainApplication.formatTime(timeStamp); } else if (moduleInfo!!.versionCode < moduleInfo!!.updateVersionCode || repoModule != null && moduleInfo!!.versionCode < repoModule!!.moduleInfo.versionCode) {
} var ignoreUpdate = false
public String getRepoName() {
if (this.repoModule == null) return "";
return this.repoModule.repoName;
}
public boolean hasFlag(int flag) {
return this.moduleInfo != null && this.moduleInfo.hasFlag(flag);
}
public Type getType() {
if (this.footerPx != -1) {
Timber.i("Module %s is footer", this.moduleId);
return Type.FOOTER;
} else if (this.separator != null) {
Timber.i("Module %s is separator", this.moduleId);
return Type.SEPARATOR;
} else if (this.notificationType != null) {
Timber.i("Module %s is notification", this.moduleId);
return Type.NOTIFICATION;
} else if (this.moduleInfo == null) {
return Type.INSTALLABLE;
} else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode || (this.repoModule != null && this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode)) {
boolean ignoreUpdate = false;
try { try {
if (Objects.requireNonNull(Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).getStringSet("pref_background_update_check_excludes", new HashSet<>())).contains(moduleInfo.id)) if (getSharedPreferences("mmm")?.getStringSet("pref_background_update_check_excludes", HashSet())!!
ignoreUpdate = true; .contains(
} catch (Exception ignored) { moduleInfo!!.id
)
) ignoreUpdate = true
} catch (ignored: Exception) {
} }
// now, we just had to make it more fucking complicated, didn't we? // now, we just had to make it more fucking complicated, didn't we?
// we now have pref_background_update_check_excludes_version, which is a id:version stringset of versions the user may want to "skip" // we now have pref_background_update_check_excludes_version, which is a id:version stringset of versions the user may want to "skip"
// oh, and because i hate myself, i made ^ at the beginning match that version and newer, and $ at the end match that version and older // oh, and because i hate myself, i made ^ at the beginning match that version and newer, and $ at the end match that version and older
Set<String> stringSetT = Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).getStringSet("pref_background_update_check_excludes_version", new HashSet<>()); val stringSetT = getSharedPreferences("mmm")?.getStringSet("pref_background_update_check_excludes_version", HashSet())
String version = ""; var version = ""
Timber.d(stringSetT.toString()); Timber.d(stringSetT.toString())
// unfortunately, stringset.contains() doesn't work for partial matches // unfortunately, stringset.contains() doesn't work for partial matches
// so we have to iterate through the set // so we have to iterate through the set
for (String s : stringSetT) { for (s in stringSetT!!) {
if (s.startsWith(this.moduleInfo.id)) { if (s.startsWith(moduleInfo!!.id)) {
version = s; version = s
Timber.d("igV: %s", version); Timber.d("igV: %s", version)
break; break
} }
} }
String remoteVersionCode = String.valueOf(moduleInfo.updateVersionCode); var remoteVersionCode = moduleInfo!!.updateVersionCode.toString()
if (repoModule != null) { if (repoModule != null) {
remoteVersionCode = String.valueOf(repoModule.moduleInfo.versionCode); remoteVersionCode = repoModule!!.moduleInfo.versionCode.toString()
} }
if (!version.isEmpty()) { if (version.isNotEmpty()) {
// now, coerce everything into an int // now, coerce everything into an int
int remoteVersionCodeInt = Integer.parseInt(remoteVersionCode); val remoteVersionCodeInt = remoteVersionCode.toInt()
int wantsVersion = Integer.parseInt(version.split(":")[1].replaceAll("[^0-9]", "")); val wantsVersion = version.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1].replace("[^0-9]".toRegex(), "").toInt()
// now find out if user wants up to and including this version, or this version and newer // now find out if user wants up to and including this version, or this version and newer
Timber.d("igV start with"); Timber.d("igV start with")
version = version.split(":")[1]; version =
version.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
// this version and newer // this version and newer
if (version.startsWith("^")) { if (version.startsWith("^")) {
Timber.d("igV: newer"); Timber.d("igV: newer")
// the wantsversion and newer // the wantsversion and newer
if (remoteVersionCodeInt >= wantsVersion) { if (remoteVersionCodeInt >= wantsVersion) {
Timber.d("igV: skipping"); Timber.d("igV: skipping")
// if it is, we skip it // if it is, we skip it
ignoreUpdate = true; ignoreUpdate = true
} }
} else if (version.endsWith("$")) { } else if (version.endsWith("$")) {
Timber.d("igV: older"); Timber.d("igV: older")
// this wantsversion and older // this wantsversion and older
if (remoteVersionCodeInt <= wantsVersion) { if (remoteVersionCodeInt <= wantsVersion) {
Timber.d("igV: skipping"); Timber.d("igV: skipping")
// if it is, we skip it // if it is, we skip it
ignoreUpdate = true; ignoreUpdate = true
} }
} else if (wantsVersion == remoteVersionCodeInt) { } else if (wantsVersion == remoteVersionCodeInt) {
Timber.d("igV: equal"); Timber.d("igV: equal")
// if it is, we skip it // if it is, we skip it
ignoreUpdate = true; ignoreUpdate = true
} }
} }
if (ignoreUpdate) { if (ignoreUpdate) {
Timber.d("Module %s has update, but is ignored", this.moduleId); Timber.d("Module %s has update, but is ignored", moduleId)
return Type.INSTALLABLE; Type.INSTALLABLE
} else { } else {
Objects.requireNonNull(MainApplication.getINSTANCE()).modulesHaveUpdates = true; INSTANCE!!.modulesHaveUpdates = true
if (!MainApplication.getINSTANCE().updateModules.contains(this.moduleId)) { if (!INSTANCE!!.updateModules.contains(moduleId)) {
MainApplication.getINSTANCE().updateModules.add(this.moduleId); INSTANCE!!.updateModules += moduleId
MainApplication.getINSTANCE().updateModuleCount++; INSTANCE!!.updateModuleCount++
} }
Timber.d("modulesHaveUpdates = %s, updateModuleCount = %s", MainApplication.getINSTANCE().modulesHaveUpdates, MainApplication.getINSTANCE().updateModuleCount); Timber.d(
Timber.d("Module %s has update", this.moduleId); "modulesHaveUpdates = %s, updateModuleCount = %s",
return Type.UPDATABLE; INSTANCE!!.modulesHaveUpdates,
INSTANCE!!.updateModuleCount
)
Timber.d("Module %s has update", moduleId)
Type.UPDATABLE
} }
} else { } else {
return Type.INSTALLED; Type.INSTALLED
}
} }
public Type getCompareType(Type type) { fun getCompareType(type: Type?): Type? {
if (this.separator != null) { return separator
return this.separator; ?: if (notificationType != null && notificationType.special) {
} else if (this.notificationType != null && this.notificationType.special) { Type.SPECIAL_NOTIFICATIONS
return Type.SPECIAL_NOTIFICATIONS;
} else { } else {
return type; type
} }
} }
public boolean shouldRemove() { fun shouldRemove(): Boolean {
if (this.repoModule != null && this.moduleInfo != null && !hasUpdate()) { if (repoModule != null && moduleInfo != null && !hasUpdate()) {
return true; return true
} }
return this.notificationType != null ? this.notificationType.shouldRemove() : this.footerPx == -1 && this.moduleInfo == null && (this.repoModule == null || !this.repoModule.repoData.isEnabled() || (PropUtils.isLowQualityModule(this.repoModule.moduleInfo) && !MainApplication.Companion.isDisableLowQualityModuleFilter())); return notificationType?.shouldRemove()
?: (footerPx == -1 && moduleInfo == null && (repoModule == null || !repoModule!!.repoData.isEnabled || isLowQualityModule(
repoModule!!.moduleInfo
) && !isDisableLowQualityModuleFilter))
} }
public void getButtons(Context context, List<ActionButtonType> buttonTypeList, boolean showcaseMode) { fun getButtons(
if (!this.isModuleHolder()) return; context: Context?,
LocalModuleInfo localModuleInfo = this.moduleInfo; buttonTypeList: MutableList<ActionButtonType?>,
showcaseMode: Boolean
) {
if (!isModuleHolder) return
val localModuleInfo = moduleInfo
// Add warning button if module id begins with a dot - this is a hidden module which could indicate malware // Add warning button if module id begins with a dot - this is a hidden module which could indicate malware
if (this.moduleId.startsWith(".") || !this.moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) { if (moduleId.startsWith(".") || !moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$".toRegex())) {
buttonTypeList.add(ActionButtonType.WARNING); buttonTypeList.add(ActionButtonType.WARNING)
} }
if (localModuleInfo != null && !showcaseMode) { if (localModuleInfo != null && !showcaseMode) {
buttonTypeList.add(ActionButtonType.UNINSTALL); buttonTypeList.add(ActionButtonType.UNINSTALL)
} }
if (this.repoModule != null && this.repoModule.notesUrl != null) { if (repoModule != null && repoModule!!.notesUrl != null) {
buttonTypeList.add(ActionButtonType.INFO); buttonTypeList.add(ActionButtonType.INFO)
} }
if ((this.repoModule != null || (localModuleInfo != null && localModuleInfo.updateZipUrl != null))) { if (repoModule != null || localModuleInfo?.updateZipUrl != null) {
buttonTypeList.add(ActionButtonType.UPDATE_INSTALL); buttonTypeList.add(ActionButtonType.UPDATE_INSTALL)
} }
String config = this.getMainModuleConfig(); val config = mainModuleConfig
if (config != null) { if (config != null) {
if (config.startsWith("https://www.androidacy.com/") && Http.hasWebView()) { if (config.startsWith("https://www.androidacy.com/") && hasWebView()) {
buttonTypeList.add(ActionButtonType.CONFIG); buttonTypeList.add(ActionButtonType.CONFIG)
} else { } else {
String pkg = IntentHelper.getPackageOfConfig(config); val pkg = getPackageOfConfig(config)
try { try {
XHooks.checkConfigTargetExists(context, pkg, config); checkConfigTargetExists(context!!, pkg, config)
buttonTypeList.add(ActionButtonType.CONFIG); buttonTypeList.add(ActionButtonType.CONFIG)
} catch (PackageManager.NameNotFoundException e) { } catch (e: PackageManager.NameNotFoundException) {
Timber.w("Config package \"" + pkg + "\" missing for module \"" + this.moduleId + "\""); Timber.w("Config package \"$pkg\" missing for module \"$moduleId\"")
} }
} }
} }
ModuleInfo moduleInfo = this.getMainModuleInfo(); var moduleInfo: ModuleInfo? = mainModuleInfo
if (moduleInfo == null) { // Avoid concurrency NPE if (moduleInfo == null) { // Avoid concurrency NPE
if (localModuleInfo == null) return; if (localModuleInfo == null) return
moduleInfo = localModuleInfo; moduleInfo = localModuleInfo
} }
if (moduleInfo.support != null) { if (moduleInfo.support != null) {
buttonTypeList.add(ActionButtonType.SUPPORT); buttonTypeList.add(ActionButtonType.SUPPORT)
} }
if (moduleInfo.donate != null) { if (moduleInfo.donate != null) {
buttonTypeList.add(ActionButtonType.DONATE); buttonTypeList.add(ActionButtonType.DONATE)
} }
if (moduleInfo.safe) { if (moduleInfo.safe) {
buttonTypeList.add(ActionButtonType.SAFE); buttonTypeList.add(ActionButtonType.SAFE)
} else { } else {
Timber.d("Module %s is not safe", this.moduleId); Timber.d("Module %s is not safe", moduleId)
} }
} }
public boolean hasUpdate() { fun hasUpdate(): Boolean {
return this.moduleInfo != null && this.repoModule != null && this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode; return moduleInfo != null && repoModule != null && moduleInfo!!.versionCode < repoModule!!.moduleInfo.versionCode
} }
@Override override operator fun compareTo(other: ModuleHolder?): Int {
public int compareTo(ModuleHolder o) {
// Compare depend on type, also allow type spoofing // Compare depend on type, also allow type spoofing
Type selfTypeReal = this.getType(); val selfTypeReal = type
Type otherTypeReal = o.getType(); val otherTypeReal = other!!.type
Type selfType = this.getCompareType(selfTypeReal); val selfType = getCompareType(selfTypeReal)
Type otherType = o.getCompareType(otherTypeReal); val otherType = other.getCompareType(otherTypeReal)
int compare = selfType.compareTo(otherType); val compare = selfType!!.compareTo(otherType!!)
return compare != 0 ? compare : selfTypeReal == otherTypeReal ? selfTypeReal.compare(this, o) : selfTypeReal.compareTo(otherTypeReal); return if (compare != 0) compare else if (selfTypeReal === otherTypeReal) selfTypeReal.compare(
} this,
other
@NonNull ) else selfTypeReal.compareTo(otherTypeReal)
@Override }
public String toString() {
return "ModuleHolder{" + "moduleId='" + moduleId + '\'' + ", notificationType=" + notificationType + ", separator=" + separator + ", footerPx=" + footerPx + '}'; override fun toString(): String {
} return "ModuleHolder{moduleId='$moduleId', notificationType=$notificationType, separator=$separator, footerPx=$footerPx}"
}
public enum Type implements Comparator<ModuleHolder> {
HEADER(R.string.loading, false, false), SEPARATOR(R.string.loading, false, false) { enum class Type(
@Override @field:StringRes @param:StringRes val title: Int,
@SuppressWarnings("ConstantConditions") val hasBackground: Boolean,
public int compare(ModuleHolder o1, ModuleHolder o2) { val moduleHolder: Boolean
return o1.separator.compareTo(o2.separator); ) : Comparator<ModuleHolder?> {
} HEADER(R.string.loading, false, false) {
}, NOTIFICATION(R.string.loading, true, false) { override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
@Override if (o1 != null && o2 != null) {
@SuppressWarnings("ConstantConditions") return o1.separator!!.compareTo(o2.separator!!)
public int compare(ModuleHolder o1, ModuleHolder o2) { } else if (o1 != null) {
return o1.notificationType.compareTo(o2.notificationType); return -1
} } else if (o2 != null) {
}, UPDATABLE(R.string.updatable, true, true) { return 1
@Override }
public int compare(ModuleHolder o1, ModuleHolder o2) { return 0
int cmp = Integer.compare(o1.filterLevel, o2.filterLevel); }
if (cmp != 0) return cmp; }, SEPARATOR(R.string.loading, false, false) {
long lastUpdated1 = o1.repoModule == null ? 0L : o1.repoModule.lastUpdated; override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
long lastUpdated2 = o2.repoModule == null ? 0L : o2.repoModule.lastUpdated; if (o1 != null && o2 != null) {
cmp = Long.compare(lastUpdated2, lastUpdated1); return o1.separator!!.compareTo(o2.separator!!)
if (cmp != 0) return cmp; } else if (o1 != null) {
return o1.getMainModuleName().compareTo(o2.getMainModuleName()); return -1
} } else if (o2 != null) {
}, INSTALLED(R.string.installed, true, true) { return 1
}
return 0
}
},
NOTIFICATION(R.string.loading, true, false) {
override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
if (o1 != null && o2 != null) {
return o1.notificationType!!.compareTo(o2.notificationType!!)
} else if (o1 != null) {
return -1
} else if (o2 != null) {
return 1
}
return 0
}
},
UPDATABLE(R.string.updatable, true, true) {
override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
if (o1 != null && o2 != null) {
var cmp = o1.filterLevel.compareTo(o2.filterLevel)
if (cmp != 0) return cmp
val lastUpdated1 =
if (o1.repoModule == null) 0L else o1.repoModule!!.lastUpdated
val lastUpdated2 =
if (o2.repoModule == null) 0L else o2.repoModule!!.lastUpdated
cmp = lastUpdated2.compareTo(lastUpdated1)
return if (cmp != 0) cmp else o1.mainModuleName!!.compareTo(o2.mainModuleName!!)
} else if (o1 != null) {
return -1
} else if (o2 != null) {
return 1
}
return 0
}
},
INSTALLED(R.string.installed, true, true) {
// get stacktrace for debugging // get stacktrace for debugging
@Override override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
public int compare(ModuleHolder o1, ModuleHolder o2) { if (o1 != null && o2 != null) {
int cmp = Integer.compare(o1.filterLevel, o2.filterLevel); val cmp = o1.filterLevel.compareTo(o2.filterLevel)
if (cmp != 0) return cmp; return if (cmp != 0) cmp else o1.mainModuleNameLowercase.compareTo(o2.mainModuleNameLowercase)
return o1.getMainModuleNameLowercase().compareTo(o2.getMainModuleNameLowercase()); } else if (o1 != null) {
} return -1
}, SPECIAL_NOTIFICATIONS(R.string.loading, true, false), INSTALLABLE(R.string.online_repo, true, true) { } else if (o2 != null) {
@Override return 1
public int compare(ModuleHolder o1, ModuleHolder o2) { }
int cmp = Integer.compare(o1.filterLevel, o2.filterLevel); return 0
if (cmp != 0) return cmp; }
long lastUpdated1 = o1.repoModule == null ? 0L : o1.repoModule.lastUpdated; },
long lastUpdated2 = o2.repoModule == null ? 0L : o2.repoModule.lastUpdated; SPECIAL_NOTIFICATIONS(R.string.loading, true, false) {
cmp = Long.compare(lastUpdated2, lastUpdated1); override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
if (cmp != 0) return cmp; if (o1 != null && o2 != null) {
return o1.getMainModuleName().compareTo(o2.getMainModuleName()); var cmp = o1.filterLevel.compareTo(o2.filterLevel)
} if (cmp != 0) return cmp
}, FOOTER(R.string.loading, false, false); val lastUpdated1 =
if (o1.repoModule == null) 0L else o1.repoModule!!.lastUpdated
@StringRes val lastUpdated2 =
public final int title; if (o2.repoModule == null) 0L else o2.repoModule!!.lastUpdated
public final boolean hasBackground; cmp = lastUpdated2.compareTo(lastUpdated1)
public final boolean moduleHolder; return if (cmp != 0) cmp else o1.mainModuleName!!.compareTo(o2.mainModuleName!!)
} else if (o1 != null) {
Type(@StringRes int title, boolean hasBackground, boolean moduleHolder) { return -1
this.title = title; } else if (o2 != null) {
this.hasBackground = hasBackground; return 1
this.moduleHolder = moduleHolder; }
} return 0
}
// Note: This method should only be called if both element have the same type }, INSTALLABLE(
@Override R.string.online_repo,
public int compare(ModuleHolder o1, ModuleHolder o2) { true,
if (o1 == o2) { true
return 0; ) {
} else if (o1 == null) { override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
return -1; if (o1 != null && o2 != null) {
} else if (o2 == null) { var cmp = o1.filterLevel.compareTo(o2.filterLevel)
return 1; if (cmp != 0) return cmp
} else { val lastUpdated1 =
return o1.moduleId.compareTo(o2.moduleId); if (o1.repoModule == null) 0L else o1.repoModule!!.lastUpdated
} val lastUpdated2 =
} if (o2.repoModule == null) 0L else o2.repoModule!!.lastUpdated
cmp = lastUpdated2.compareTo(lastUpdated1)
return if (cmp != 0) cmp else o1.mainModuleName!!.compareTo(o2.mainModuleName!!)
} else if (o1 != null) {
return -1
} else if (o2 != null) {
return 1
}
return 0
}
},
FOOTER(R.string.loading, false, false) {
override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int {
if (o1 != null && o2 != null) {
return o1.footerPx.compareTo(o2.footerPx)
} else if (o1 != null) {
return -1
} else if (o2 != null) {
return 1
}
return 0
}
};
} }
} }

@ -56,8 +56,8 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
} }
} }
} }
} catch (ignored: Exception) { } catch (e: Exception) {
Timber.e("Error while updating module holder. This may mean we're trying to update too early.") Timber.e(e, "Error while updating module holder. This may mean we're trying to update too early.")
} }
} }
@ -138,7 +138,7 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
if (initState) return@setOnClickListener // Skip if non user if (initState) return@setOnClickListener // Skip if non user
val moduleHolder = moduleHolder val moduleHolder = moduleHolder
if (i < actionButtonsTypes.size && moduleHolder != null) { if (i < actionButtonsTypes.size && moduleHolder != null) {
actionButtonsTypes[i]!!.doAction(v as Chip?, moduleHolder) (v as Chip?)?.let { actionButtonsTypes[i]!!.doAction(it, moduleHolder) }
if (moduleHolder.shouldRemove()) { if (moduleHolder.shouldRemove()) {
cardView.visibility = View.GONE cardView.visibility = View.GONE
} }
@ -149,8 +149,10 @@ class ModuleViewAdapter : RecyclerView.Adapter<ModuleViewAdapter.ViewHolder>() {
val moduleHolder = moduleHolder val moduleHolder = moduleHolder
var didSomething = false var didSomething = false
if (i < actionButtonsTypes.size && moduleHolder != null) { if (i < actionButtonsTypes.size && moduleHolder != null) {
didSomething = actionButtonsTypes[i]!! didSomething = (v as Chip?)?.let {
.doActionLong(v as Chip?, moduleHolder) actionButtonsTypes[i]!!
.doActionLong(it, moduleHolder)
} == true
if (moduleHolder.shouldRemove()) { if (moduleHolder.shouldRemove()) {
cardView.visibility = View.GONE cardView.visibility = View.GONE
} }

@ -72,6 +72,8 @@ class ModuleViewListBuilder(private val activity: Activity) {
Timber.i("appendRemoteModules() called") Timber.i("appendRemoteModules() called")
} }
synchronized(updateLock) { synchronized(updateLock) {
Timber.i("appendRemoteModules() started")
val startTime = System.currentTimeMillis()
val showIncompatible = MainApplication.isShowIncompatibleModules val showIncompatible = MainApplication.isShowIncompatibleModules
for (moduleHolder in mappedModuleHolders.values) { for (moduleHolder in mappedModuleHolders.values) {
moduleHolder.repoModule = null moduleHolder.repoModule = null
@ -119,6 +121,7 @@ class ModuleViewListBuilder(private val activity: Activity) {
} }
} }
} }
Timber.i("appendRemoteModules() finished in %dms", System.currentTimeMillis() - startTime)
} }
} }
} }

@ -10,12 +10,13 @@ import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences
import com.fox2code.mmm.utils.io.Hashes.Companion.hashSha256 import com.fox2code.mmm.utils.io.Hashes.Companion.hashSha256
import com.fox2code.mmm.utils.io.PropUtils.Companion.isNullString import com.fox2code.mmm.utils.io.PropUtils.Companion.isNullString
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import com.fox2code.mmm.utils.room.ReposList
import com.fox2code.mmm.utils.room.ReposListDatabase import com.fox2code.mmm.utils.room.ReposListDatabase
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@Suppress("UNUSED_PARAMETER", "MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
class CustomRepoManager internal constructor( class CustomRepoManager internal constructor(
mainApplication: MainApplication?, private val repoManager: RepoManager mainApplication: MainApplication?, private val repoManager: RepoManager
) { ) {
@ -35,7 +36,7 @@ class CustomRepoManager internal constructor(
// now the same as above but for room database // now the same as above but for room database
val applicationContext = mainApplication!!.applicationContext val applicationContext = mainApplication!!.applicationContext
val db = Room.databaseBuilder( val db = Room.databaseBuilder(
applicationContext, ReposListDatabase::class.java, "reposlist.db" applicationContext, ReposListDatabase::class.java, "ReposList.db"
).build() ).build()
val reposListDao = db.reposListDao() val reposListDao = db.reposListDao()
val reposListList = reposListDao.getAll() val reposListList = reposListDao.getAll()
@ -49,6 +50,7 @@ class CustomRepoManager internal constructor(
(repoManager.addOrGet(repo) as CustomRepoData).override = "custom_repo_$index" (repoManager.addOrGet(repo) as CustomRepoData).override = "custom_repo_$index"
} }
} }
db.close()
} }
} }
@ -113,7 +115,8 @@ class CustomRepoManager internal constructor(
applicationContext, ReposListDatabase::class.java, "reposlist.db" applicationContext, ReposListDatabase::class.java, "reposlist.db"
).build() ).build()
val reposListDao = db.reposListDao() val reposListDao = db.reposListDao()
reposListDao.insert(id, repo, true, donate, support, submitModule, 0, name, website) val reposList = ReposList(id, repo, true, donate, support, submitModule, 0, name, website)
reposListDao.insert(reposList)
repoCount++ repoCount++
dirty = true dirty = true
val customRepoData = repoManager.addOrGet(repo) as CustomRepoData val customRepoData = repoManager.addOrGet(repo) as CustomRepoData
@ -127,6 +130,7 @@ class CustomRepoManager internal constructor(
// Set the enabled state to true // Set the enabled state to true
customRepoData.isEnabled = true customRepoData.isEnabled = true
customRepoData.updateEnabledState() customRepoData.updateEnabledState()
db.close()
return customRepoData return customRepoData
} }

@ -41,8 +41,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
private var metaDataCache: JSONObject? private var metaDataCache: JSONObject?
@JvmField @JvmField
var lastUpdate var lastUpdate: Long = 0
: Long = 0
@JvmField @JvmField
var website: String? = null var website: String? = null
@ -72,18 +71,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
var defaultSubmitModule: String? = null var defaultSubmitModule: String? = null
override var name: String? = null override var name: String? = null
get() = { get() = field ?: defaultName
// if name is null return defaultName and if defaultName is null return url
if (field == null) {
if (defaultName == null) {
url
} else {
defaultName
}
} else {
field
}
}.toString()
set(value) { set(value) {
field = value field = value
} }
@ -150,9 +138,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
isForceHide = shouldForceHide(tempVarForPreferenceId) isForceHide = shouldForceHide(tempVarForPreferenceId)
// basically same as above but for room database // basically same as above but for room database
val db = Room.databaseBuilder( val db = Room.databaseBuilder(
INSTANCE!!.applicationContext, INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db"
ReposListDatabase::class.java,
"repo_database"
).allowMainThreadQueries().build() ).allowMainThreadQueries().build()
val reposListRoom = db.reposListDao() val reposListRoom = db.reposListDao()
val reposListRoomList = reposListRoom.getById(preferenceId!!) val reposListRoomList = reposListRoom.getById(preferenceId!!)
@ -189,6 +175,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
Timber.w("Failed to load repo metadata from database: " + e.message + ". If this is a first time run, this is normal.") Timber.w("Failed to load repo metadata from database: " + e.message + ". If this is a first time run, this is normal.")
} }
} }
db.close()
} }
open fun prepare(): Boolean { open fun prepare(): Boolean {
@ -204,8 +191,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
var nameForModules = var nameForModules =
if (name.endsWith(" (Official)")) name.substring(0, name.length - 11) else name if (name.endsWith(" (Official)")) name.substring(0, name.length - 11) else name
nameForModules = if (nameForModules.endsWith(" [Official]")) nameForModules.substring( nameForModules = if (nameForModules.endsWith(" [Official]")) nameForModules.substring(
0, 0, nameForModules.length - 11
nameForModules.length - 11
) else nameForModules ) else nameForModules
nameForModules = nameForModules =
if (nameForModules.contains("Official")) nameForModules.replace("Official", "") if (nameForModules.contains("Official")) nameForModules.replace("Official", "")
@ -309,7 +295,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
INSTANCE!!.applicationContext, INSTANCE!!.applicationContext,
ReposListDatabase::class.java, ReposListDatabase::class.java,
"ReposList.db", "ReposList.db",
).build() ).allowMainThreadQueries().build()
val reposList = db.reposListDao().getById(preferenceId!!) val reposList = db.reposListDao().getById(preferenceId!!)
// should never happen but for safety // should never happen but for safety
if (reposList.enabled) { if (reposList.enabled) {
@ -325,9 +311,21 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
INSTANCE!!.applicationContext, INSTANCE!!.applicationContext,
ReposListDatabase::class.java, ReposListDatabase::class.java,
"ReposList.db", "ReposList.db",
).build() ).allowMainThreadQueries().build()
val reposList = db.reposListDao().getById(preferenceId!!) val reposList = db.reposListDao().getById(preferenceId!!)
db.reposListDao().update(name = reposList.name, enabled = value, id = reposList.id, donate = reposList.donate, support = reposList.support, website = reposList.website, submitModule = reposList.submitModule, lastUpdate = reposList.lastUpdate.toLong(), url = reposList.url) db.reposListDao().update(
name = reposList.name,
enabled = value,
id = reposList.id,
donate = reposList.donate.toString(),
support = reposList.support.toString(),
website = reposList.website.toString(),
submitModule = reposList.submitModule.toString(),
lastUpdate = reposList.lastUpdate.toLong(),
url = reposList.url
)
db.close()
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -381,6 +379,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
ReposListDatabase::class.java, ReposListDatabase::class.java,
"ReposList.db", "ReposList.db",
).allowMainThreadQueries().build() ).allowMainThreadQueries().build()
val reposList = db.reposListDao().getById(preferenceId!!) val reposList = db.reposListDao().getById(preferenceId!!)
enabled = if (reposList.enabled) { enabled = if (reposList.enabled) {
!isForceHide !isForceHide
@ -433,10 +432,14 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() {
val diff = currentTime - lastUpdate val diff = currentTime - lastUpdate
val diffMinutes = diff / (60 * 1000) % 60 val diffMinutes = diff / (60 * 1000) % 60
Timber.d("Repo $preferenceId updated: $diffMinutes minutes ago") Timber.d("Repo $preferenceId updated: $diffMinutes minutes ago")
db.close()
db2.close()
diffMinutes > if (BuildConfig.DEBUG) 15 else 30 diffMinutes > if (BuildConfig.DEBUG) 15 else 30
} else { } else {
Timber.d("Repo $preferenceId should update could not find repo in database") Timber.d("Repo $preferenceId should update could not find repo in database")
Timber.d("This is probably an error, please report this to the developer") Timber.d("This is probably an error, please report this to the developer")
db.close()
db2.close()
true true
} }
} else { } else {

@ -7,6 +7,7 @@ package com.fox2code.mmm.repo
import androidx.room.Room import androidx.room.Room
import com.fox2code.mmm.MainApplication import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import com.fox2code.mmm.utils.room.ModuleListCache
import com.fox2code.mmm.utils.room.ModuleListCacheDatabase import com.fox2code.mmm.utils.room.ModuleListCacheDatabase
import com.fox2code.mmm.utils.room.ReposListDatabase import com.fox2code.mmm.utils.room.ReposListDatabase
import org.json.JSONArray import org.json.JSONArray
@ -127,6 +128,10 @@ class RepoUpdater(repoData2: RepoData) {
return true return true
} }
if (indexRaw != null) { if (indexRaw != null) {
// new thread to update the database
val thread = Thread {
val startTime = System.currentTimeMillis()
Timber.d("Updating database for %s", repoData.preferenceId)
try { try {
// iterate over modules, using this.supportedProperties as a template to attempt to get each property from the module. everything that is not null is added to the module // iterate over modules, using this.supportedProperties as a template to attempt to get each property from the module. everything that is not null is added to the module
// use room to insert to // use room to insert to
@ -135,29 +140,7 @@ class RepoUpdater(repoData2: RepoData) {
MainApplication.INSTANCE!!, MainApplication.INSTANCE!!,
ModuleListCacheDatabase::class.java, ModuleListCacheDatabase::class.java,
"ModuleListCache.db" "ModuleListCache.db"
).allowMainThreadQueries().build() ).build()
// array with module info default values
// supported properties for a module
//id=<string>
//name=<string>
//version=<string>
//versionCode=<int>
//author=<string>
//description=<string>
//minApi=<int>
//maxApi=<int>
//minMagisk=<int>
//needRamdisk=<boolean>
//support=<url>
//donate=<url>
//config=<package>
//changeBoot=<boolean>
//mmtReborn=<boolean>
// extra properties only useful for the database
//repoId=<string>
//installed=<boolean>
//installedVersionCode=<int> (only if installed)
//
// all except first six can be null // all except first six can be null
// this.indexRaw is the raw index file (json) // this.indexRaw is the raw index file (json)
val modules = JSONObject(String(indexRaw!!, StandardCharsets.UTF_8)) val modules = JSONObject(String(indexRaw!!, StandardCharsets.UTF_8))
@ -284,12 +267,39 @@ class RepoUpdater(repoData2: RepoData) {
} }
} }
} }
moduleListCacheDao.insert(name = name, version = version, versionCode = versionCode, author = author, description = description, minApi = minApiInt, maxApi = maxApiInt, minMagisk = minMagiskInt, needRamdisk = needRamdisk, support = support ?: "", donate = donate ?: "", config = config ?: "", changeBoot = changeBoot, mmtReborn = mmtReborn, repoId = repoId!!, safe = safe, lastUpdate = lastUpdate.toLong(), stats = downloads, codename = id ?: "") val moduleListCache = ModuleListCache(
name = name,
version = version,
versionCode = versionCode,
author = author,
description = description,
minApi = minApiInt,
maxApi = maxApiInt,
minMagisk = minMagiskInt,
needRamdisk = needRamdisk,
support = support ?: "",
donate = donate ?: "",
config = config ?: "",
changeBoot = changeBoot,
mmtReborn = mmtReborn,
repoId = repoId!!,
safe = safe,
lastUpdate = lastUpdate.toLong(),
stats = downloads,
codename = id ?: ""
)
moduleListCacheDao.insert(moduleListCache)
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
} }
db.close()
val endTime = System.currentTimeMillis()
val timeTaken = endTime - startTime
Timber.d("Time taken to parse modules: $timeTaken ms")
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
}
thread.start()
indexRaw = null indexRaw = null
// set lastUpdate // set lastUpdate
val db = Room.databaseBuilder( val db = Room.databaseBuilder(
@ -300,6 +310,7 @@ class RepoUpdater(repoData2: RepoData) {
val repoListDao = db.reposListDao() val repoListDao = db.reposListDao()
repoListDao.setLastUpdate(repoData.preferenceId!!, System.currentTimeMillis()) repoListDao.setLastUpdate(repoData.preferenceId!!, System.currentTimeMillis())
db.close() db.close()
success.set(true)
} else { } else {
success.set(true) // assume we're reading from cache. this may be unsafe but it's better than nothing success.set(true) // assume we're reading from cache. this may be unsafe but it's better than nothing
} }

@ -5,7 +5,6 @@
package com.fox2code.mmm.settings; package com.fox2code.mmm.settings;
import static com.fox2code.mmm.settings.SettingsActivity.RepoFragment.applyMaterial3; import static com.fox2code.mmm.settings.SettingsActivity.RepoFragment.applyMaterial3;
import static java.lang.Integer.parseInt;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ActivityManager; import android.app.ActivityManager;
@ -98,12 +97,12 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
@ -136,35 +135,30 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
@PerformanceClass @PerformanceClass
public static int getDevicePerformanceClass() { public static int getDevicePerformanceClass() {
int devicePerformanceClass; // special algorithm to determine performance class. low is < 4 cores and/ore < 4GB ram, mid is 4-6 cores and 4-6GB ram, high is > 6 cores and > 6GB ram. android sdk version is used as well
int androidVersion = Build.VERSION.SDK_INT; // device is awarded 1 point for each core and 1 point for each GB of ram.
int cpuCount = Runtime.getRuntime().availableProcessors();
int memoryClass = ((ActivityManager) Objects.requireNonNull(MainApplication.getINSTANCE()).getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
int totalCpuFreq = 0;
int freqResolved = 0;
for (int i = 0; i < cpuCount; i++) {
try (RandomAccessFile reader = new RandomAccessFile(String.format(Locale.ENGLISH, "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i), "r")) {
String line = reader.readLine();
if (line != null) {
totalCpuFreq += parseInt(line) / 1000;
freqResolved++;
}
} catch (Exception ignore) {
}
}
int maxCpuFreq = freqResolved == 0 ? -1 : (int) Math.ceil(totalCpuFreq / (float) freqResolved);
if (androidVersion < 21 || cpuCount <= 2 || memoryClass <= 100 || cpuCount <= 4 && maxCpuFreq != -1 && maxCpuFreq <= 1250 || cpuCount <= 4 && maxCpuFreq <= 1600 && memoryClass <= 128 && androidVersion == 21 || cpuCount <= 4 && maxCpuFreq <= 1300 && memoryClass <= 128 && androidVersion <= 24) { int points = 0;
devicePerformanceClass = PERFORMANCE_CLASS_LOW; int cores = Runtime.getRuntime().availableProcessors();
} else if (cpuCount < 8 || memoryClass <= 160 || maxCpuFreq != -1 && maxCpuFreq <= 2050 || maxCpuFreq == -1 && cpuCount == 8 && androidVersion <= 23) { ActivityManager activityManager = (ActivityManager) Objects.requireNonNull(MainApplication.getINSTANCE()).getSystemService(Context.ACTIVITY_SERVICE);
devicePerformanceClass = PERFORMANCE_CLASS_AVERAGE; if (activityManager != null) {
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
long totalMemory = memoryInfo.totalMem;
points += cores;
points += totalMemory / 1024 / 1024 / 1024;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
points += 1;
}
Timber.d("Device performance class: %d", points);
if (points <= 9) {
return PERFORMANCE_CLASS_LOW;
} else if (points <= 12) {
return PERFORMANCE_CLASS_AVERAGE;
} else { } else {
devicePerformanceClass = PERFORMANCE_CLASS_HIGH; return PERFORMANCE_CLASS_HIGH;
} }
Timber.d("getDevicePerformanceClass: androidVersion=" + androidVersion + " cpuCount=" + cpuCount + " memoryClass=" + memoryClass + " maxCpuFreq=" + maxCpuFreq + " devicePerformanceClass=" + devicePerformanceClass);
return devicePerformanceClass;
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@ -1072,6 +1066,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
} }
// Get magisk_alt_repo enabled state from room reposlist db // Get magisk_alt_repo enabled state from room reposlist db
ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build();
// add listener to magisk_alt_repo_enabled switch to update room db // add listener to magisk_alt_repo_enabled switch to update room db
Preference magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled")); Preference magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled"));
magiskAltRepoEnabled.setOnPreferenceChangeListener((preference, newValue) -> { magiskAltRepoEnabled.setOnPreferenceChangeListener((preference, newValue) -> {
@ -1259,17 +1254,17 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
public void updateCustomRepoList(boolean initial) { public void updateCustomRepoList(boolean initial) {
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
// get all repos that are not built-in // get all repos that are not built-in
int CUSTOM_REPO_ENTRIES = 0; int CUSTOM_REPO_ENTRIES = 0;
// array of custom repos // array of custom repos
ArrayList<String> customRepos = new ArrayList<>(); ArrayList<String> customRepos = new ArrayList<>();
RealmResults<ReposList> customRepoDataDB = realm.where(ReposList.class).findAll(); ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build();
for (ReposList repo : customRepoDataDB) { List<ReposList> reposList = db.reposListDao().getAll();
if (!repo.getId().equals("androidacy_repo") && !repo.getId().equals("magisk_alt_repo")) { for (ReposList repo : reposList) {
ArrayList<String> buildInRepos = new ArrayList<>(Arrays.asList("androidacy_repo", "magisk_alt_repo"));
if (!buildInRepos.contains(repo.getId())) {
CUSTOM_REPO_ENTRIES++; CUSTOM_REPO_ENTRIES++;
customRepos.add(repo.getUrl()); customRepos.add(repo.getId());
} }
} }
Timber.d("%d repos: %s", CUSTOM_REPO_ENTRIES, customRepos); Timber.d("%d repos: %s", CUSTOM_REPO_ENTRIES, customRepos);
@ -1286,12 +1281,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (preference == null) continue; if (preference == null) continue;
final int index = i; final int index = i;
preference.setOnPreferenceClickListener(preference1 -> { preference.setOnPreferenceClickListener(preference1 -> {
if (realm.isInTransaction()) { db.reposListDao().delete(customRepos.get(index));
realm.commitTransaction();
}
realm.beginTransaction();
Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findFirst()).deleteFromRealm();
realm.commitTransaction();
customRepoManager.removeRepo(index); customRepoManager.removeRepo(index);
updateCustomRepoList(false); updateCustomRepoList(false);
preference1.setVisible(false); preference1.setVisible(false);
@ -1428,16 +1418,15 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (preference == null) return; if (preference == null) return;
if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) { if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) {
if (repoData != null) { if (repoData != null) {
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build();
Realm realm = Realm.getInstance(realmConfiguration); ReposList reposList = db.reposListDao().getById(repoData.preferenceId);
RealmResults<ReposList> repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findAll();
Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo");
if (repoData.isForceHide() || repoDataRealmResults.isEmpty()) { if (repoData.isForceHide() || reposList == null) {
Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden"); Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden");
hideRepoData(preferenceName); hideRepoData(preferenceName);
return; return;
} else { } else {
Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), repoDataRealmResults.toString()); Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), reposList);
preference.setTitle(repoData.getName()); preference.setTitle(repoData.getName());
preference.setVisible(true); preference.setVisible(true);
// set website, support, and submitmodule as well as donate // set website, support, and submitmodule as well as donate
@ -1474,7 +1463,6 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
findPreference(preferenceName + "_donate").setVisible(false); findPreference(preferenceName + "_donate").setVisible(false);
} }
} }
realm.close();
} else { } else {
Timber.d("Hiding preference " + preferenceName + " because it's data is null"); Timber.d("Hiding preference " + preferenceName + " because it's data is null");
hideRepoData(preferenceName); hideRepoData(preferenceName);

@ -382,10 +382,15 @@ enum class PropUtils {
InputStreamReader(inputStream, StandardCharsets.UTF_8) InputStreamReader(inputStream, StandardCharsets.UTF_8)
).use { bufferedReader -> ).use { bufferedReader ->
var line: String var line: String
while (bufferedReader.readLine().also { line = it } != null) { var lineNumber = 0
val iterator = bufferedReader.lineSequence().iterator()
while (iterator.hasNext()) {
line = iterator.next()
lineNumber++
while (line.startsWith("\u0000")) line = line.substring(1) while (line.startsWith("\u0000")) line = line.substring(1)
if (line.startsWith("$what=")) { if (line.startsWith("$what=")) {
moduleId = line.substring(what.length + 1).trim { it <= ' ' } moduleId = line.substring(what.length + 1).trim { it <= ' ' }
break
} }
} }
} }

@ -7,7 +7,7 @@ package com.fox2code.mmm.utils.room
import androidx.room.Entity import androidx.room.Entity
@Suppress("unused") @Suppress("unused")
@Entity(tableName = "modulelistcache") @Entity(tableName = "modulelistcache", primaryKeys = ["codename"], indices = [androidx.room.Index(value = ["codename"], unique = true)])
class ModuleListCache ( class ModuleListCache (
var codename: String, var codename: String,
var version: String, var version: String,

@ -69,7 +69,7 @@ interface ModuleListCacheDao {
fun getByCodename(codename: String): ModuleListCache fun getByCodename(codename: String): ModuleListCache
@Insert(entity = ModuleListCache::class, onConflict = OnConflictStrategy.REPLACE) @Insert(entity = ModuleListCache::class, onConflict = OnConflictStrategy.REPLACE)
fun insert(codename: String, version: String, versionCode: Int, author: String, description: String, minApi: Int, maxApi: Int, minMagisk: Int, needRamdisk: Boolean, support: String, donate: String, config: String, changeBoot: Boolean, mmtReborn: Boolean, repoId: String, lastUpdate: Long, safe: Boolean, name: String, stats: Int) fun insert(moduleListCache: ModuleListCache)
@Query("UPDATE modulelistcache SET version = :version WHERE codename = :codename") @Query("UPDATE modulelistcache SET version = :version WHERE codename = :codename")
fun setVersion(codename: String, version: String) fun setVersion(codename: String, version: String)

@ -5,11 +5,10 @@
package com.fox2code.mmm.utils.room package com.fox2code.mmm.utils.room
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "ReposList") @Entity(tableName = "ReposList", primaryKeys = ["id"], indices = [androidx.room.Index(value = ["id"], unique = true)])
data class ReposList( data class ReposList(
@PrimaryKey var id: String, var id: String,
var url: String, var url: String,
var enabled: Boolean, var enabled: Boolean,
var donate: String?, var donate: String?,

@ -5,6 +5,8 @@
package com.fox2code.mmm.utils.room package com.fox2code.mmm.utils.room
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
@Suppress("unused") @Suppress("unused")
@ -26,11 +28,11 @@ interface ReposListDao {
@Query("SELECT * FROM ReposList WHERE id = :id") @Query("SELECT * FROM ReposList WHERE id = :id")
fun getById(id: String): ReposList fun getById(id: String): ReposList
@Query("INSERT INTO ReposList VALUES (:id, :url, :enabled, :donate, :support, :submitModule, :lastUpdate, :name, :website)") @Insert(entity = ReposList::class, onConflict = OnConflictStrategy.REPLACE)
fun insert(id: String, url: String, enabled: Boolean, donate: String?, support: String?, submitModule: String?, lastUpdate: Long, name: String, website: String?) fun insert(reposList: ReposList)
@Query("UPDATE ReposList SET url = :url, enabled = :enabled, donate = :donate, support = :support, submitModule = :submitModule, lastUpdate = :lastUpdate, name = :name, website = :website WHERE id = :id") @Query("UPDATE ReposList SET url = :url, enabled = :enabled, donate = :donate, support = :support, submitModule = :submitModule, lastUpdate = :lastUpdate, name = :name, website = :website WHERE id = :id")
fun update(id: String, url: String?, enabled: Boolean?, donate: String?, support: String?, submitModule: String?, lastUpdate: Long?, name: String?, website: String?) fun update(id: String = "", url: String = "", enabled: Boolean = false, donate: String = "", support: String = "", submitModule: String = "", lastUpdate: Long = 0, name: String = "", website: String = "")
@Query("DELETE FROM ReposList WHERE id = :id") @Query("DELETE FROM ReposList WHERE id = :id")
fun delete(id: String) fun delete(id: String)

@ -439,4 +439,6 @@
<string name="reinstall_warning">Most modules should be uninstalled, then reinstalled cleanly. For this reason, AMM does not support reinstalling modules while they are installed on the device.</string> <string name="reinstall_warning">Most modules should be uninstalled, then reinstalled cleanly. For this reason, AMM does not support reinstalling modules while they are installed on the device.</string>
<string name="build_expired">This build has expired. Please update to the latest version.</string> <string name="build_expired">This build has expired. Please update to the latest version.</string>
<string name="eula_agree_v2_headline">EULA and Terms</string> <string name="eula_agree_v2_headline">EULA and Terms</string>
<string name="error_creating_repos_database">Could not create repos db</string>
<string name="error_creating_modulelistcache_database">Failed to create module cache db</string>
</resources> </resources>

@ -5,8 +5,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
repositories { repositories {
mavenCentral()
google() google()
mavenCentral()
maven { maven {
setUrl("https://jitpack.io") setUrl("https://jitpack.io")
} }

Loading…
Cancel
Save