diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 43205ae..de57598 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,6 +72,10 @@ android { "en" ) ) + // ksp room processor + room { + schemaLocationDir.set(file("roomSchemas")) + } } splits { @@ -525,9 +529,6 @@ dependencies { // yes implementation("com.github.fingerprintjs:fingerprint-android:2.0.0") - // encryption for room - implementation("net.zetetic:android-database-sqlcipher:4.5.4") - // room implementation("androidx.room:room-runtime:2.5.1") diff --git a/app/roomSchemas/com.fox2code.mmm.utils.room.ModuleListCacheDatabase/1.json b/app/roomSchemas/com.fox2code.mmm.utils.room.ModuleListCacheDatabase/1.json new file mode 100644 index 0000000..f38c4d2 --- /dev/null +++ b/app/roomSchemas/com.fox2code.mmm.utils.room.ModuleListCacheDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/roomSchemas/com.fox2code.mmm.utils.room.ReposListDatabase/1.json b/app/roomSchemas/com.fox2code.mmm.utils.room.ReposListDatabase/1.json new file mode 100644 index 0000000..5fb155a --- /dev/null +++ b/app/roomSchemas/com.fox2code.mmm.utils.room.ReposListDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/assets/modulelistcache.db b/app/src/main/assets/modulelistcache.db deleted file mode 100644 index d638fcd..0000000 Binary files a/app/src/main/assets/modulelistcache.db and /dev/null differ diff --git a/app/src/main/assets/repos.db b/app/src/main/assets/repos.db deleted file mode 100644 index 368f41f..0000000 Binary files a/app/src/main/assets/repos.db and /dev/null differ diff --git a/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt index 2220b17..de146ca 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/MainActivity.kt @@ -28,7 +28,6 @@ import androidx.appcompat.widget.SearchView import androidx.cardview.widget.CardView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import androidx.room.Room import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener 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.io.net.Http.Companion.cleanDnsCache 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.progressindicator.LinearProgressIndicator import org.matomo.sdk.extra.TrackHelper @@ -102,25 +100,6 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis onMainActivityCreate(this) super.onCreate(savedInstanceState) 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 if (BuildConfig.ENABLE_PROTECTION && !MainApplication.o && !BuildConfig.DEBUG) { throw RuntimeException("This is not an official build of AMM") diff --git a/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt b/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt index 42627fd..fcb85e8 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/MainApplication.kt @@ -14,9 +14,6 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build import android.os.SystemClock -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import android.util.Base64 import android.util.Log import androidx.annotation.StyleRes import androidx.core.app.NotificationManagerCompat @@ -44,35 +41,16 @@ import io.noties.markwon.Markwon import io.noties.markwon.html.HtmlPlugin import io.noties.markwon.image.ImagesPlugin import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler -import io.realm.Realm import org.matomo.sdk.Matomo import org.matomo.sdk.Tracker import org.matomo.sdk.TrackerBuilder import org.matomo.sdk.extra.TrackHelper import timber.log.Timber 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.UnrecoverableKeyException -import java.security.cert.CertificateException import java.text.SimpleDateFormat import java.util.Date 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.max @@ -94,7 +72,7 @@ class MainApplication : FoxApplication(), Configuration.Provider { @JvmField var markwon: Markwon? = null - private var existingKey: ByteArray? = null + private var existingKey: CharArray? = null @JvmField var tracker: Tracker? = null @@ -106,6 +84,38 @@ class MainApplication : FoxApplication(), Configuration.Provider { 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? { if (isCrashHandler) return null if (markwon != null) return markwon @@ -178,8 +188,7 @@ class MainApplication : FoxApplication(), Configuration.Provider { @SuppressLint("NonConstantResourceId") override fun isLightTheme(): Boolean { - return when (getSharedPreferences("mmm")!! - .getString("pref_theme", "system")) { + return when (getSharedPreferences("mmm")!!.getString("pref_theme", "system")) { "system" -> isSystemLightTheme "dark", "black" -> false else -> true @@ -253,9 +262,6 @@ class MainApplication : FoxApplication(), Configuration.Provider { Timber.d("Initializing AMM") Timber.d("Started from background: %s", !isInForeground) Timber.d("AMM is running in debug mode") - Timber.d("Initializing Realm") - Realm.init(this) - Timber.d("Initialized Realm") // analytics Timber.d("Initializing matomo") getTracker() @@ -265,22 +271,18 @@ class MainApplication : FoxApplication(), Configuration.Provider { } else { tracker!!.isOptOut = false } - if (getSharedPreferences("matomo")!! - .getBoolean("install_tracked", false) - ) { + if (getSharedPreferences("matomo")!!.getBoolean("install_tracked", false)) { TrackHelper.track().download().with(INSTANCE!!.getTracker()) Timber.d("Sent install event to matomo") - getSharedPreferences("matomo")!! - .edit().putBoolean("install_tracked", true).apply() + getSharedPreferences("matomo")!!.edit().putBoolean("install_tracked", true).apply() } else { Timber.d("Matomo already has install") } try { - @Suppress("DEPRECATION") - @SuppressLint("PackageManagerGetSignatures") val s = this.packageManager.getPackageInfo( - this.packageName, - PackageManager.GET_SIGNATURES - ).signatures + @Suppress("DEPRECATION") @SuppressLint("PackageManagerGetSignatures") val s = + this.packageManager.getPackageInfo( + this.packageName, PackageManager.GET_SIGNATURES + ).signatures @Suppress("SpellCheckingInspection") val osh = arrayOf( "7bec7c4462f4aac616612d9f56a023ee3046e83afa956463b5fab547fd0a0be6", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", @@ -322,8 +324,7 @@ class MainApplication : FoxApplication(), Configuration.Provider { Timber.i("Emoji compat loaded!") }, "Emoji compat init.").start() } - @Suppress("KotlinConstantConditions") - if ((BuildConfig.ANDROIDACY_CLIENT_ID == "")) { + @Suppress("KotlinConstantConditions") if ((BuildConfig.ANDROIDACY_CLIENT_ID == "")) { Timber.w("Androidacy client id is empty! Please set it in androidacy.properties. Will not enable Androidacy.") val editor = sharedPreferences!!.edit() editor.putBoolean("pref_androidacy_repo_enabled", false) @@ -371,8 +372,7 @@ class MainApplication : FoxApplication(), Configuration.Provider { if (!dataDir.exists()) { if (!dataDir.mkdirs()) { if (BuildConfig.DEBUG) Timber.w( - "Failed to create directory %s", - dataDir + "Failed to create directory %s", dataDir ) } } @@ -413,9 +413,7 @@ class MainApplication : FoxApplication(), Configuration.Provider { val packageName = this.packageName for (appProcess in appProcesses) { Timber.d( - "Process: %s, Importance: %d", - appProcess.processName, - appProcess.importance + "Process: %s, Importance: %d", appProcess.processName, appProcess.importance ) if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) { return true @@ -435,237 +433,6 @@ class MainApplication : FoxApplication(), Configuration.Provider { } }// 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() { modulesHaveUpdates = false @@ -728,12 +495,11 @@ class MainApplication : FoxApplication(), Configuration.Provider { var updateCheckBg: String? = null init { - Shell.setDefaultBuilder( - Shell.Builder.create() - .setFlags(Shell.FLAG_REDIRECT_STDERR or Shell.FLAG_MOUNT_MASTER).setTimeout(15) - .setInitializers( - InstallerInitializer::class.java - ).also { shellBuilder = it }) + Shell.setDefaultBuilder(Shell.Builder.create() + .setFlags(Shell.FLAG_REDIRECT_STDERR or Shell.FLAG_MOUNT_MASTER).setTimeout(15) + .setInitializers( + InstallerInitializer::class.java + ).also { shellBuilder = it }) val random = Random() do { secret = random.nextLong() @@ -749,8 +515,7 @@ class MainApplication : FoxApplication(), Configuration.Provider { val packageName = componentName?.packageName ?: (intent.getPackage())!! require( !(!BuildConfig.APPLICATION_ID.equals( - packageName, - ignoreCase = true + packageName, ignoreCase = true ) && relPackageName != packageName) ) { // 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. - */if (BuildConfig.DEBUG) { + */ + if (BuildConfig.DEBUG) { // get file, function, and line number val stackTrace = Thread.currentThread().stackTrace // 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 callers.add(name + ":" + caller.lineNumber + ":" + caller.methodName) // get the last 3 callers - val last3: List = - callers.subList(max(callers.size - 3, 0), callers.size) + val last3: List = 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 (((last3.size == 3) && last3[0] == last3[1]) && last3[1] == last3[2]) { Timber.e( @@ -834,50 +599,49 @@ class MainApplication : FoxApplication(), Configuration.Provider { // convert from String to boolean return java.lang.Boolean.parseBoolean(SHOWCASE_MODE_TRUE) } - val showcaseMode = getSharedPreferences("mmm")!! - .getBoolean("pref_showcase_mode", false) + val showcaseMode = + getSharedPreferences("mmm")!!.getBoolean("pref_showcase_mode", false) SHOWCASE_MODE_TRUE = showcaseMode.toString() return showcaseMode } fun shouldPreventReboot(): Boolean { - return getSharedPreferences("mmm")!! - .getBoolean("pref_prevent_reboot", true) + return getSharedPreferences("mmm")!!.getBoolean("pref_prevent_reboot", true) } val isShowIncompatibleModules: Boolean - get() = getSharedPreferences("mmm")!! - .getBoolean("pref_show_incompatible", false) + get() = getSharedPreferences("mmm")!!.getBoolean("pref_show_incompatible", false) val isForceDarkTerminal: Boolean - get() = getSharedPreferences("mmm")!! - .getBoolean("pref_force_dark_terminal", false) + get() = getSharedPreferences("mmm")!!.getBoolean("pref_force_dark_terminal", false) val isTextWrapEnabled: Boolean - get() = getSharedPreferences("mmm")!! - .getBoolean("pref_wrap_text", false) + get() = getSharedPreferences("mmm")!!.getBoolean("pref_wrap_text", false) val isDohEnabled: Boolean - get() = getSharedPreferences("mmm")!! - .getBoolean("pref_dns_over_https", true) + get() = getSharedPreferences("mmm")!!.getBoolean("pref_dns_over_https", true) val isMonetEnabled: Boolean - get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences("mmm")!! - .getBoolean("pref_enable_monet", true) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences("mmm")!!.getBoolean( + "pref_enable_monet", + true + ) val isBlurEnabled: Boolean - get() = getSharedPreferences("mmm")!! - .getBoolean("pref_enable_blur", false) + get() = getSharedPreferences("mmm")!!.getBoolean("pref_enable_blur", false) @JvmStatic val isDeveloper: Boolean get() { - return if (BuildConfig.DEBUG) true else getSharedPreferences("mmm")!! - .getBoolean("developer", false) + return if (BuildConfig.DEBUG) true else getSharedPreferences("mmm")!!.getBoolean( + "developer", + false + ) } val isDisableLowQualityModuleFilter: Boolean - get() = getSharedPreferences("mmm")!! - .getBoolean("pref_disable_low_quality_module_filter", false) && isDeveloper + get() = getSharedPreferences("mmm")!!.getBoolean( + "pref_disable_low_quality_module_filter", + false + ) && isDeveloper val isUsingMagiskCommand: Boolean get() = (peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND) && getSharedPreferences( "mmm" - )!! - .getBoolean("pref_use_magisk_install_command", false) && isDeveloper + )!!.getBoolean("pref_use_magisk_install_command", false) && isDeveloper @JvmStatic val isBackgroundUpdateCheckEnabled: Boolean @@ -886,23 +650,28 @@ class MainApplication : FoxApplication(), Configuration.Provider { return java.lang.Boolean.parseBoolean(updateCheckBg) } val wrapped = isWrapped - val updateCheckBgTemp = !wrapped && getSharedPreferences("mmm")!! - .getBoolean("pref_background_update_check", true) + val updateCheckBgTemp = !wrapped && getSharedPreferences("mmm")!!.getBoolean( + "pref_background_update_check", + true + ) updateCheckBg = updateCheckBgTemp.toString() return java.lang.Boolean.parseBoolean(updateCheckBg) } val isAndroidacyTestMode: Boolean - get() = isDeveloper && getSharedPreferences("mmm")!! - .getBoolean("pref_androidacy_test_mode", false) + get() = isDeveloper && getSharedPreferences("mmm")!!.getBoolean( + "pref_androidacy_test_mode", + false + ) fun setHasGottenRootAccess(bool: Boolean) { - getSharedPreferences("mmm")!! - .edit().putBoolean("has_root_access", bool).apply() + getSharedPreferences("mmm")!!.edit().putBoolean("has_root_access", bool).apply() } val isCrashReportingEnabled: Boolean - get() = SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm")!! - .getBoolean("pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING) + get() = SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm")!!.getBoolean( + "pref_crash_reporting", + BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING + ) val bootSharedPreferences: SharedPreferences? get() = getSharedPreferences("mmm_boot") @@ -918,8 +687,10 @@ class MainApplication : FoxApplication(), Configuration.Provider { @JvmStatic fun isMatomoAllowed(): Boolean { - return getSharedPreferences("mmm")!! - .getBoolean("pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS) + return getSharedPreferences("mmm")!!.getBoolean( + "pref_analytics_enabled", + BuildConfig.DEFAULT_ENABLE_ANALYTICS + ) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt b/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt index e550098..a574319 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/SetupActivity.kt @@ -21,8 +21,11 @@ import androidx.fragment.app.FragmentActivity import androidx.room.Room import com.fox2code.foxcompat.app.FoxActivity import com.fox2code.mmm.databinding.ActivitySetupBinding +import com.fox2code.mmm.repo.RepoManager 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.ReposList import com.fox2code.mmm.utils.room.ReposListDatabase import com.fox2code.rosettax.LanguageActivity import com.fox2code.rosettax.LanguageSwitcher @@ -227,10 +230,11 @@ class SetupActivity : FoxActivity(), LanguageActivity { val db = Room.databaseBuilder( applicationContext, ReposListDatabase::class.java, "ReposList.db" - ).build() + ).allowMainThreadQueries().build() val androidacyRepoRoom = andRepoView.isChecked val magiskAltRepoRoom = magiskAltRepoView.isChecked val reposListDao = db.reposListDao() + Timber.d(reposListDao.getAll().toString()) val androidacyRepoRoomObj = reposListDao.getById("androidacy_repo") val magiskAltRepoRoomObj = reposListDao.getById("magisk_alt_repo") reposListDao.setEnabled(androidacyRepoRoomObj.id, androidacyRepoRoom) @@ -239,12 +243,6 @@ class SetupActivity : FoxActivity(), LanguageActivity { editor.putString("last_shown_setup", "v3") // Commit the changes editor.commit() - // sleep to allow the realm transaction to finish - try { - Thread.sleep(250) - } catch (e: InterruptedException) { - Thread.currentThread().interrupt() - } // Log the changes Timber.d("Setup finished. Preferences: %s", prefs.all) Timber.d("Androidacy repo: %s", androidacyRepoRoom) @@ -329,18 +327,111 @@ class SetupActivity : FoxActivity(), LanguageActivity { // creates the room database private fun createDatabases() { - val startTime = System.currentTimeMillis() - val appContext = MainApplication.INSTANCE!!.applicationContext - Room.databaseBuilder(appContext, ReposListDatabase::class.java, "reposlist.db") - .createFromAsset("assets/reposlist.db") - .fallbackToDestructiveMigration() - .build() - // same for modulelistcache - Room.databaseBuilder(appContext, ModuleListCacheDatabase::class.java, "modulelistcache.db") - .createFromAsset("assets/modulelistcache.db") - .fallbackToDestructiveMigration() - .build() - Timber.d("Databases created in %s ms", System.currentTimeMillis() - startTime) + val thread = Thread { + Timber.d("Creating databases") + val startTime = System.currentTimeMillis() + val appContext = MainApplication.INSTANCE!!.applicationContext + val db = Room.databaseBuilder(appContext, ReposListDatabase::class.java, "ReposList.db") + .fallbackToDestructiveMigration().build() + // same for modulelistcache + val db2 = Room.databaseBuilder( + appContext, + ModuleListCacheDatabase::class.java, + "ModuleListCache.db" + ) + .fallbackToDestructiveMigration() + .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) + } + thread.start() } private fun createFiles() { diff --git a/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt b/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt index cd18bd0..df42edc 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt @@ -66,12 +66,11 @@ class ModuleManager private constructor() : SyncManager() { var moduleInfo = moduleInfos[module] // next, merge the module info with a record from ModuleListCache room db if it exists // initialize modulelistcache db - // DO NOT USE REALM ANYMORE val db = Room.databaseBuilder( MainApplication.INSTANCE!!, ModuleListCacheDatabase::class.java, "ModuleListCache" - ).build() + ).allowMainThreadQueries().build() // get module info from cache val moduleListCacheDao: ModuleListCacheDao = db.moduleListCacheDao() Timber.d("Found cache for %s", module) diff --git a/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt b/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt index 44bbf48..50d50f3 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/module/ActionButtonType.kt @@ -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. */ +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; -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") +@Suppress("SENSELESS_COMPARISON") @SuppressLint("UseCompatLoadingForDrawables") -public enum ActionButtonType { - INFO() { - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_info_24)); - button.setText(R.string.description); +enum class ActionButtonType { + INFO { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + button.chipIcon = button.context.getDrawable(R.drawable.ic_baseline_info_24) + button.setText(R.string.description) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker()); - String notesUrl = moduleHolder.repoModule.notesUrl; - if (AndroidacyUtil.Companion.isAndroidacyLink(notesUrl)) { - IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig()); + TrackHelper.track().event("view_notes", name).with(INSTANCE!!.getTracker()) + val notesUrl = moduleHolder.repoModule?.notesUrl + if (isAndroidacyLink(notesUrl)) { + openUrlAndroidacy( + button.context, + notesUrl, + false, + moduleHolder.repoModule?.moduleInfo?.name, + moduleHolder.mainModuleConfig + ) } 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 - public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { - Context context = button.getContext(); - Toast.makeText(context, context.getString(R.string.module_id_prefix) + moduleHolder.moduleId, Toast.LENGTH_SHORT).show(); - return true; + override fun doActionLong(button: Chip, moduleHolder: ModuleHolder): Boolean { + val context = button.context + Toast.makeText( + context, + context.getString(R.string.module_id_prefix) + moduleHolder.moduleId, + Toast.LENGTH_SHORT + ).show() + return true } - }, UPDATE_INSTALL() { - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - int icon; + }, + UPDATE_INSTALL { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + val icon: Int if (moduleHolder.hasUpdate()) { - icon = R.drawable.ic_baseline_update_24; - button.setText(R.string.update); + icon = R.drawable.ic_baseline_update_24 + button.setText(R.string.update) } else if (moduleHolder.moduleInfo != null) { - icon = R.drawable.ic_baseline_refresh_24; - button.setText(R.string.reinstall); + icon = R.drawable.ic_baseline_refresh_24 + button.setText(R.string.reinstall) } else { - icon = R.drawable.ic_baseline_system_update_24; - button.setText(R.string.install); + icon = R.drawable.ic_baseline_system_update_24 + button.setText(R.string.install) } - button.setChipIcon(button.getContext().getDrawable(icon)); + button.chipIcon = button.context.getDrawable(icon) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); - if (moduleInfo == null) return; - - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + // if mainmoduleinfo is null, we are in repo mode + val moduleInfo: ModuleInfo = if (moduleHolder.mainModuleInfo != null) { + moduleHolder.mainModuleInfo } 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 (moduleHolder.moduleInfo != null) { // get icon of the button - Drawable icon = button.getChipIcon(); - if (icon != null && icon.getConstantState() != null) { - Drawable reinstallIcon = button.getContext().getDrawable(R.drawable.ic_baseline_refresh_24); - if (reinstallIcon != null && reinstallIcon.getConstantState() != null) { - if (icon.getConstantState().equals(reinstallIcon.getConstantState())) { - new MaterialAlertDialogBuilder(button.getContext()) - .setTitle(R.string.reinstall) - .setMessage(R.string.reinstall_warning) - .setPositiveButton(R.string.reinstall, null) - .show(); - return; + val icon = button.chipIcon + if (icon != null && icon.constantState != null) { + val reinstallIcon = + button.context.getDrawable(R.drawable.ic_baseline_refresh_24) + if (reinstallIcon != null && reinstallIcon.constantState != null) { + if (icon.constantState == reinstallIcon.constantState) { + MaterialAlertDialogBuilder(button.context) + .setTitle(R.string.reinstall) + .setMessage(R.string.reinstall_warning) + .setPositiveButton(R.string.reinstall, null) + .show() + return } } } } - String updateZipUrl = moduleHolder.getUpdateZipUrl(); - if (updateZipUrl == null) return; + val updateZipUrl = moduleHolder.updateZipUrl ?: return // Androidacy manage the selection between download and install - if (AndroidacyUtil.Companion.isAndroidacyLink(updateZipUrl)) { - IntentHelper.openUrlAndroidacy(button.getContext(), updateZipUrl, true, moduleInfo.name, moduleInfo.config); - return; + if (isAndroidacyLink(updateZipUrl)) { + openUrlAndroidacy( + button.context, + updateZipUrl, + true, + moduleInfo.name, + moduleInfo.config + ) + return } - boolean hasRoot = InstallerInitializer.peekMagiskPath() != null && !MainApplication.Companion.isShowcaseMode(); - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(button.getContext()); - builder.setTitle(moduleInfo.name).setCancelable(true).setIcon(R.drawable.ic_baseline_extension_24); - CharSequence desc = moduleInfo.description; - Markwon markwon = null; - LocalModuleInfo localModuleInfo = moduleHolder.moduleInfo; - if (localModuleInfo != null && !localModuleInfo.updateChangeLog.isEmpty()) { - markwon = MainApplication.getINSTANCE().getMarkwon(); + val hasRoot = peekMagiskPath() != null && !isShowcaseMode + 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 - desc = markwon.toMarkdown(localModuleInfo.updateChangeLog); + desc = markwon!!.toMarkdown(localModuleInfo.updateChangeLog) } - - if (desc == null || desc.length() == 0) { - builder.setMessage(R.string.no_desc_found); + if (desc.isNullOrEmpty()) { + builder.setMessage(R.string.no_desc_found) } else { - builder.setMessage(desc); + builder.setMessage(desc) + } + Timber.i("URL: %s", updateZipUrl) + builder.setNegativeButton(R.string.download_module) { _: DialogInterface?, _: Int -> + openCustomTab( + button.context, + updateZipUrl + ) } - Timber.i("URL: %s", updateZipUrl); - builder.setNegativeButton(R.string.download_module, (x, y) -> IntentHelper.openCustomTab(button.getContext(), updateZipUrl)); if (hasRoot) { - builder.setPositiveButton(moduleHolder.hasUpdate() ? R.string.update_module : R.string.install_module, (x, y) -> { - String updateZipChecksum = moduleHolder.getUpdateZipChecksum(); - IntentHelper.openInstaller(button.getContext(), updateZipUrl, moduleInfo.name, moduleInfo.config, updateZipChecksum, moduleInfo.mmtReborn); - }); + builder.setPositiveButton(if (moduleHolder.hasUpdate()) R.string.update_module else R.string.install_module) { _: DialogInterface?, _: Int -> + val updateZipChecksum = moduleHolder.updateZipChecksum + openInstaller( + button.context, + updateZipUrl, + moduleInfo.name, + moduleInfo.config, + updateZipChecksum, + moduleInfo.mmtReborn + ) + } } - ExternalHelper.INSTANCE.injectButton(builder, () -> Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo()); - int dim5dp = FoxDisplay.dpToPixel(5); - builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp); - AlertDialog alertDialog = builder.show(); - for (int i = -3; i < 0; i++) { - Button alertButton = alertDialog.getButton(i); - if (alertButton != null && alertButton.getPaddingStart() > dim5dp) { - alertButton.setPadding(dim5dp, dim5dp, dim5dp, dim5dp); + 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) { - TextView messageView = Objects.requireNonNull(alertDialog.getWindow()).findViewById(android.R.id.message); - markwon.setParsedMarkdown(messageView, (Spanned) desc); + val messageView = alertDialog.window!!.findViewById(android.R.id.message) + markwon.setParsedMarkdown(messageView, (desc as Spanned?)!!) } } - }, UNINSTALL() { - @Override - public void update(Chip button, 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; - button.setChipIcon(button.getContext().getDrawable(icon)); - button.setText(R.string.uninstall); + }, + UNINSTALL { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + val icon = + if (moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING)) R.drawable.ic_baseline_delete_outline_24 else if (!moduleHolder.hasFlag( + 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 - public void doAction(Chip button, ModuleHolder moduleHolder) { - if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UNINSTALLING) && moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) { - doActionLong(button, moduleHolder); - return; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE or ModuleInfo.FLAG_MODULE_UNINSTALLING) && moduleHolder.hasFlag( + ModuleInfo.FLAG_MODULE_UPDATING + ) + ) { + doActionLong(button, moduleHolder) + return } - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("uninstall_module", name).with(MainApplication.getINSTANCE().getTracker()); - Timber.i(Integer.toHexString(moduleHolder.moduleInfo.flags)); - if (!Objects.requireNonNull(ModuleManager.getInstance()).setUninstallState(moduleHolder.moduleInfo, !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) { - Timber.e("Failed to switch uninstalled state!"); + TrackHelper.track().event("uninstall_module", name).with(INSTANCE!!.getTracker()) + Timber.i(Integer.toHexString(moduleHolder.moduleInfo?.flags ?: 0)) + if (!instance!!.setUninstallState( + moduleHolder.moduleInfo!!, !moduleHolder.hasFlag( + ModuleInfo.FLAG_MODULE_UNINSTALLING + ) + ) + ) { + Timber.e("Failed to switch uninstalled state!") } - update(button, moduleHolder); + update(button, moduleHolder) } - @Override - public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { + override fun doActionLong(button: Chip, moduleHolder: ModuleHolder): Boolean { + if (moduleHolder.moduleInfo == null) { + return false + } // Actually a module having mount is the only issue when deleting module - if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) - return false; // We can't trust active flag on first boot - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(button.getContext()); - builder.setTitle(R.string.master_delete); - builder.setPositiveButton(R.string.master_delete_yes, (dialog, which) -> { - String moduleId = moduleHolder.moduleInfo.id; - if (!Objects.requireNonNull(ModuleManager.getInstance()).masterClear(moduleHolder.moduleInfo)) { - Toast.makeText(button.getContext(), R.string.master_delete_fail, Toast.LENGTH_SHORT).show(); + if (moduleHolder.moduleInfo!!.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) return false // We can't trust active flag on first boot + val builder = MaterialAlertDialogBuilder(button.context) + builder.setTitle(R.string.master_delete) + builder.setPositiveButton(R.string.master_delete_yes) { _: DialogInterface?, _: Int -> + val moduleId = moduleHolder.moduleInfo!!.id + if (!instance!!.masterClear(moduleHolder.moduleInfo!!)) { + Toast.makeText(button.context, R.string.master_delete_fail, Toast.LENGTH_SHORT) + .show() } else { - moduleHolder.moduleInfo = null; - FoxActivity.getFoxActivity(button).refreshUI(); - Timber.e("Cleared: %s", moduleId); + moduleHolder.moduleInfo = null + FoxActivity.getFoxActivity(button).refreshUI() + Timber.e("Cleared: %s", moduleId) } - }); - builder.setNegativeButton(R.string.master_delete_no, (v, i) -> { - }); - builder.create(); - builder.show(); - return true; + } + builder.setNegativeButton(R.string.master_delete_no) { _: DialogInterface?, _: Int -> } + builder.create() + builder.show() + return true } - }, CONFIG() { - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_app_settings_alt_24)); - button.setText(R.string.config); + }, + CONFIG { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + button.chipIcon = + button.context.getDrawable(R.drawable.ic_baseline_app_settings_alt_24) + button.setText(R.string.config) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - String config = moduleHolder.getMainModuleConfig(); - if (config == null) return; - - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + if (moduleHolder.moduleInfo == null) { + return + } + val config = moduleHolder.mainModuleConfig ?: return + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("config_module", name).with(MainApplication.getINSTANCE().getTracker()); - if (AndroidacyUtil.Companion.isAndroidacyLink(config)) { - IntentHelper.openUrlAndroidacy(button.getContext(), config, true); + TrackHelper.track().event("config_module", name).with(INSTANCE!!.getTracker()) + if (isAndroidacyLink(config)) { + openUrlAndroidacy(button.context, config, true) } else { - IntentHelper.openConfig(button.getContext(), config); + openConfig(button.context, config) } } - }, SUPPORT() { - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); - button.setChipIcon(button.getContext().getDrawable(supportIconForUrl(moduleInfo.support))); - button.setText(R.string.support); + }, + SUPPORT { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + val moduleInfo = moduleHolder.mainModuleInfo + button.chipIcon = button.context.getDrawable( + supportIconForUrl(moduleInfo.support) + ) + button.setText(R.string.support) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("support_module", name).with(MainApplication.getINSTANCE().getTracker()); - IntentHelper.Companion.openUrl(button.getContext(), Objects.requireNonNull(moduleHolder.getMainModuleInfo().support)); + TrackHelper.track().event("support_module", name).with(INSTANCE!!.getTracker()) + openUrl(button.context, Objects.requireNonNull(moduleHolder.mainModuleInfo.support)) } - }, DONATE() { - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); - button.setChipIcon(button.getContext().getDrawable(donateIconForUrl(moduleInfo.donate))); - button.setText(R.string.donate); + }, + DONATE { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + val moduleInfo = moduleHolder.mainModuleInfo + button.chipIcon = button.context.getDrawable( + donateIconForUrl(moduleInfo.donate) + ) + button.setText(R.string.donate) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("donate_module", name).with(MainApplication.getINSTANCE().getTracker()); - IntentHelper.Companion.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate); + TrackHelper.track().event("donate_module", name).with(INSTANCE!!.getTracker()) + openUrl(button.context, moduleHolder.mainModuleInfo.donate) } - }, WARNING() { - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_warning_24)); - button.setText(R.string.warning); + }, + WARNING { + override fun update(button: Chip, moduleHolder: ModuleHolder) { + button.chipIcon = button.context.getDrawable(R.drawable.ic_baseline_warning_24) + button.setText(R.string.warning) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("warning_module", name).with(MainApplication.getINSTANCE().getTracker()); - new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.warning).setMessage(R.string.warning_message).setPositiveButton(R.string.understand, (v, i) -> { - }).create().show(); + TrackHelper.track().event("warning_module", name).with(INSTANCE!!.getTracker()) + MaterialAlertDialogBuilder(button.context).setTitle(R.string.warning) + .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 - @Override - public void update(Chip button, ModuleHolder moduleHolder) { - button.setChipIcon(button.getContext().getDrawable(R.drawable.baseline_verified_user_24)); - button.setText(R.string.safe); + override fun update(button: Chip, moduleHolder: ModuleHolder) { + button.chipIcon = + button.context.getDrawable(R.drawable.baseline_verified_user_24) + button.setText(R.string.safe) } - @Override - public void doAction(Chip button, ModuleHolder moduleHolder) { - String name; - if (moduleHolder.moduleInfo != null) { - name = moduleHolder.moduleInfo.name; + override fun doAction(button: Chip, moduleHolder: ModuleHolder) { + val name: String? = if (moduleHolder.moduleInfo != null) { + moduleHolder.moduleInfo!!.name } else { - name = moduleHolder.repoModule.moduleInfo.name; + moduleHolder.repoModule?.moduleInfo?.name } - TrackHelper.track().event("safe_module", name).with(MainApplication.getINSTANCE().getTracker()); - new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.safe_module).setMessage(R.string.safe_message).setPositiveButton(R.string.understand, (v, i) -> { - }).create().show(); + TrackHelper.track().event("safe_module", name).with(INSTANCE!!.getTracker()) + MaterialAlertDialogBuilder(button.context).setTitle(R.string.safe_module) + .setMessage(R.string.safe_message).setPositiveButton( + R.string.understand + ) { _: DialogInterface?, _: Int -> } + .create().show() } }; @DrawableRes - private final int iconId; + private val iconId: Int - ActionButtonType() { - this.iconId = 0; + constructor() { + iconId = 0 } - @SuppressWarnings("unused") - ActionButtonType(int iconId) { - this.iconId = iconId; + @Suppress("unused") + constructor(iconId: Int) { + this.iconId = iconId } - @DrawableRes - public static int supportIconForUrl(String url) { - int icon = R.drawable.ic_baseline_support_24; - if (url == null) { - return icon; - } else if (url.startsWith("https://t.me/")) { - icon = R.drawable.ic_baseline_telegram_24; - } else if (url.startsWith("https://discord.gg/") || url.startsWith("https://discord.com/invite/")) { - icon = R.drawable.ic_baseline_discord_24; - } else if (url.startsWith("https://github.com/")) { - icon = R.drawable.ic_github; - } else if (url.startsWith("https://gitlab.com/")) { - icon = R.drawable.ic_gitlab; - } else if (url.startsWith("https://forum.xda-developers.com/")) { - icon = R.drawable.ic_xda; - } - return icon; + open fun update(button: Chip, moduleHolder: ModuleHolder) { + button.chipIcon = button.context.getDrawable(iconId) } - @DrawableRes - public static int donateIconForUrl(String url) { - int icon = R.drawable.ic_baseline_monetization_on_24; - if (url == null) { - return icon; - } else if (url.startsWith("https://www.paypal.me/") || url.startsWith("https://www.paypal.com/paypalme/") || url.startsWith("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/")) { - icon = R.drawable.ic_patreon; - } - return icon; - } - - public void update(Chip button, ModuleHolder moduleHolder) { - button.setChipIcon(button.getContext().getDrawable(this.iconId)); + abstract fun doAction(button: Chip, moduleHolder: ModuleHolder) + open fun doActionLong(button: Chip, moduleHolder: ModuleHolder): Boolean { + return false } - public abstract void doAction(Chip button, ModuleHolder moduleHolder); + companion object { + @JvmStatic + @DrawableRes + fun supportIconForUrl(url: String?): Int { + var icon = R.drawable.ic_baseline_support_24 + if (url == null) { + return icon + } else if (url.startsWith("https://t.me/")) { + icon = R.drawable.ic_baseline_telegram_24 + } else if (url.startsWith("https://discord.gg/") || url.startsWith("https://discord.com/invite/")) { + icon = R.drawable.ic_baseline_discord_24 + } else if (url.startsWith("https://github.com/")) { + icon = R.drawable.ic_github + } else if (url.startsWith("https://gitlab.com/")) { + icon = R.drawable.ic_gitlab + } else if (url.startsWith("https://forum.xda-developers.com/")) { + icon = R.drawable.ic_xda + } + return icon + } - public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { - return false; + @JvmStatic + @DrawableRes + fun donateIconForUrl(url: String?): Int { + var icon = R.drawable.ic_baseline_monetization_on_24 + if (url == null) { + return icon + } else if (url.startsWith("https://www.paypal.me/") || url.startsWith("https://www.paypal.com/paypalme/") || url.startsWith( + "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/")) { + icon = R.drawable.ic_patreon + } + return icon + } } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/module/ModuleHolder.kt b/app/src/main/kotlin/com/fox2code/mmm/module/ModuleHolder.kt index f82ecd5..477e0c0 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/module/ModuleHolder.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/module/ModuleHolder.kt @@ -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. */ - -package com.fox2code.mmm.module; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; - -import com.fox2code.mmm.MainApplication; -import com.fox2code.mmm.NotificationType; -import com.fox2code.mmm.R; -import com.fox2code.mmm.XHooks; -import com.fox2code.mmm.manager.LocalModuleInfo; -import com.fox2code.mmm.manager.ModuleInfo; -import com.fox2code.mmm.repo.RepoModule; -import com.fox2code.mmm.utils.IntentHelper; -import com.fox2code.mmm.utils.io.PropUtils; -import com.fox2code.mmm.utils.io.net.Http; - -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.Set; - -import timber.log.Timber; - -public final class ModuleHolder implements Comparable { - - public final String moduleId; - public final NotificationType notificationType; - public final Type separator; - public int footerPx; - public View.OnClickListener onClickListener; - - public LocalModuleInfo moduleInfo; - public RepoModule repoModule; - public int filterLevel; - - public ModuleHolder(String moduleId) { - this.moduleId = Objects.requireNonNull(moduleId); - this.notificationType = null; - this.separator = null; - this.footerPx = -1; - } - - public ModuleHolder(NotificationType notificationType) { - this.moduleId = ""; - this.notificationType = Objects.requireNonNull(notificationType); - this.separator = null; - this.footerPx = -1; - } - - public ModuleHolder(Type separator) { - this.moduleId = ""; - this.notificationType = null; - this.separator = separator; - this.footerPx = -1; +package com.fox2code.mmm.module + +import android.content.Context +import android.content.pm.PackageManager +import android.view.View +import androidx.annotation.StringRes +import com.fox2code.mmm.MainApplication.Companion.INSTANCE +import com.fox2code.mmm.MainApplication.Companion.formatTime +import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences +import com.fox2code.mmm.MainApplication.Companion.isDisableLowQualityModuleFilter +import com.fox2code.mmm.NotificationType +import com.fox2code.mmm.R +import com.fox2code.mmm.XHooks.Companion.checkConfigTargetExists +import com.fox2code.mmm.manager.LocalModuleInfo +import com.fox2code.mmm.manager.ModuleInfo +import com.fox2code.mmm.repo.RepoModule +import com.fox2code.mmm.utils.IntentHelper.Companion.getPackageOfConfig +import com.fox2code.mmm.utils.io.PropUtils.Companion.isLowQualityModule +import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView +import timber.log.Timber +import java.util.Objects + +@Suppress("unused", "KotlinConstantConditions") +class ModuleHolder : Comparable { + val moduleId: String + val notificationType: NotificationType? + val separator: Type? + var footerPx: Int + var onClickListener: View.OnClickListener? = null + var moduleInfo: LocalModuleInfo? = null + var repoModule: RepoModule? = null + var filterLevel = 0 + + constructor(moduleId: String) { + this.moduleId = Objects.requireNonNull(moduleId) + notificationType = null + separator = null + footerPx = -1 } - @SuppressWarnings("unused") - public ModuleHolder(int footerPx, boolean header) { - this.moduleId = ""; - this.notificationType = null; - this.separator = null; - this.footerPx = footerPx; - this.filterLevel = header ? 1 : 0; + constructor(notificationType: NotificationType) { + moduleId = "" + this.notificationType = Objects.requireNonNull(notificationType) + separator = null + footerPx = -1 } - public boolean isModuleHolder() { - return this.notificationType == null && this.separator == null && this.footerPx == -1; + constructor(separator: Type?) { + moduleId = "" + notificationType = null + this.separator = separator + footerPx = -1 } - public ModuleInfo getMainModuleInfo() { - return this.repoModule != null && (this.moduleInfo == null || this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.moduleInfo : this.moduleInfo; + @Suppress("unused") + constructor(footerPx: Int, header: Boolean) { + moduleId = "" + notificationType = null + separator = null + this.footerPx = footerPx + filterLevel = if (header) 1 else 0 } - public String getUpdateZipUrl() { - return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.zipUrl : this.moduleInfo.updateZipUrl; - } - - public String getUpdateZipRepo() { - return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.repoData.preferenceId : "update_json"; - } - - public String getUpdateZipChecksum() { - return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.checksum : this.moduleInfo.updateChecksum; - } - - public String getMainModuleName() { - ModuleInfo moduleInfo = this.getMainModuleInfo(); - if (moduleInfo == null || moduleInfo.name == null) - throw new Error("Error for " + this.getType().name() + " id " + this.moduleId); - return moduleInfo.name; - } - - public String getMainModuleNameLowercase() { - return this.getMainModuleName().toLowerCase(Locale.ROOT); - } - - public String getMainModuleConfig() { - if (this.moduleInfo == null) return null; - String config = this.moduleInfo.config; - if (config == null && this.repoModule != null) { - config = this.repoModule.moduleInfo.config; + val isModuleHolder: Boolean + get() = notificationType == null && separator == null && footerPx == -1 + val mainModuleInfo: ModuleInfo + get() = if (repoModule != null && (moduleInfo == null || moduleInfo!!.versionCode < repoModule!!.moduleInfo.versionCode)) repoModule!!.moduleInfo else moduleInfo!! + val updateZipUrl: String? + get() = if (moduleInfo == null || repoModule != null && moduleInfo!!.updateVersionCode < repoModule!!.moduleInfo.versionCode) repoModule!!.zipUrl else moduleInfo!!.updateZipUrl + val updateZipRepo: String? + get() = if (moduleInfo == null || repoModule != null && moduleInfo!!.updateVersionCode < repoModule!!.moduleInfo.versionCode) repoModule!!.repoData.preferenceId else "update_json" + val updateZipChecksum: String? + get() = if (moduleInfo == null || repoModule != null && moduleInfo!!.updateVersionCode < repoModule!!.moduleInfo.versionCode) repoModule!!.checksum else moduleInfo!!.updateChecksum + val mainModuleName: String? + get() { + val moduleInfo = mainModuleInfo + if (moduleInfo.name == null) throw Error("Error for " + type.name + " id " + moduleId) + return moduleInfo.name } - return config; - } - - public String getUpdateTimeText() { - if (this.repoModule == null) return ""; - long timeStamp = this.repoModule.lastUpdated; - return timeStamp <= 0 ? "" : MainApplication.formatTime(timeStamp); - } - - public String getRepoName() { - if (this.repoModule == null) return ""; - return this.repoModule.repoName; - } + val mainModuleNameLowercase: String + get() = mainModuleName!!.lowercase() + val mainModuleConfig: String? + get() { + if (moduleInfo == null) return null + var config = moduleInfo!!.config + if (config == null && repoModule != null) { + config = repoModule!!.moduleInfo.config + } + return config + } + val updateTimeText: String + get() { + if (repoModule == null) return "" + val timeStamp = repoModule!!.lastUpdated + return if (timeStamp <= 0) "" else formatTime(timeStamp) + } + val repoName: String? + get() = if (repoModule == null) "" else repoModule!!.repoName - public boolean hasFlag(int flag) { - return this.moduleInfo != null && this.moduleInfo.hasFlag(flag); + fun hasFlag(flag: Int): Boolean { + return moduleInfo != null && 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; + val type: Type + get() = if (footerPx != -1) { + Timber.i("Module %s is footer", moduleId) + Type.FOOTER + } else if (separator != null) { + Timber.i("Module %s is separator", moduleId) + Type.SEPARATOR + } else if (notificationType != null) { + Timber.i("Module %s is notification", moduleId) + Type.NOTIFICATION + } else if (moduleInfo == null) { + Type.INSTALLABLE + } else if (moduleInfo!!.versionCode < moduleInfo!!.updateVersionCode || repoModule != null && moduleInfo!!.versionCode < repoModule!!.moduleInfo.versionCode) { + var ignoreUpdate = false try { - if (Objects.requireNonNull(Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).getStringSet("pref_background_update_check_excludes", new HashSet<>())).contains(moduleInfo.id)) - ignoreUpdate = true; - } catch (Exception ignored) { + if (getSharedPreferences("mmm")?.getStringSet("pref_background_update_check_excludes", HashSet())!! + .contains( + moduleInfo!!.id + ) + ) ignoreUpdate = true + } catch (ignored: Exception) { } // 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" // 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 stringSetT = Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).getStringSet("pref_background_update_check_excludes_version", new HashSet<>()); - String version = ""; - Timber.d(stringSetT.toString()); + val stringSetT = getSharedPreferences("mmm")?.getStringSet("pref_background_update_check_excludes_version", HashSet()) + var version = "" + Timber.d(stringSetT.toString()) // unfortunately, stringset.contains() doesn't work for partial matches // so we have to iterate through the set - for (String s : stringSetT) { - if (s.startsWith(this.moduleInfo.id)) { - version = s; - Timber.d("igV: %s", version); - break; + for (s in stringSetT!!) { + if (s.startsWith(moduleInfo!!.id)) { + version = s + Timber.d("igV: %s", version) + break } } - String remoteVersionCode = String.valueOf(moduleInfo.updateVersionCode); + var remoteVersionCode = moduleInfo!!.updateVersionCode.toString() 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 - int remoteVersionCodeInt = Integer.parseInt(remoteVersionCode); - int wantsVersion = Integer.parseInt(version.split(":")[1].replaceAll("[^0-9]", "")); + val remoteVersionCodeInt = remoteVersionCode.toInt() + 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 - Timber.d("igV start with"); - version = version.split(":")[1]; + Timber.d("igV start with") + version = + version.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1] // this version and newer if (version.startsWith("^")) { - Timber.d("igV: newer"); + Timber.d("igV: newer") // the wantsversion and newer if (remoteVersionCodeInt >= wantsVersion) { - Timber.d("igV: skipping"); + Timber.d("igV: skipping") // if it is, we skip it - ignoreUpdate = true; + ignoreUpdate = true } } else if (version.endsWith("$")) { - Timber.d("igV: older"); + Timber.d("igV: older") // this wantsversion and older if (remoteVersionCodeInt <= wantsVersion) { - Timber.d("igV: skipping"); + Timber.d("igV: skipping") // if it is, we skip it - ignoreUpdate = true; + ignoreUpdate = true } } else if (wantsVersion == remoteVersionCodeInt) { - Timber.d("igV: equal"); + Timber.d("igV: equal") // if it is, we skip it - ignoreUpdate = true; + ignoreUpdate = true } - } if (ignoreUpdate) { - Timber.d("Module %s has update, but is ignored", this.moduleId); - return Type.INSTALLABLE; + Timber.d("Module %s has update, but is ignored", moduleId) + Type.INSTALLABLE } else { - Objects.requireNonNull(MainApplication.getINSTANCE()).modulesHaveUpdates = true; - if (!MainApplication.getINSTANCE().updateModules.contains(this.moduleId)) { - MainApplication.getINSTANCE().updateModules.add(this.moduleId); - MainApplication.getINSTANCE().updateModuleCount++; + INSTANCE!!.modulesHaveUpdates = true + if (!INSTANCE!!.updateModules.contains(moduleId)) { + INSTANCE!!.updateModules += moduleId + INSTANCE!!.updateModuleCount++ } - Timber.d("modulesHaveUpdates = %s, updateModuleCount = %s", MainApplication.getINSTANCE().modulesHaveUpdates, MainApplication.getINSTANCE().updateModuleCount); - Timber.d("Module %s has update", this.moduleId); - return Type.UPDATABLE; + Timber.d( + "modulesHaveUpdates = %s, updateModuleCount = %s", + INSTANCE!!.modulesHaveUpdates, + INSTANCE!!.updateModuleCount + ) + Timber.d("Module %s has update", moduleId) + Type.UPDATABLE } } else { - return Type.INSTALLED; + Type.INSTALLED } - } - public Type getCompareType(Type type) { - if (this.separator != null) { - return this.separator; - } else if (this.notificationType != null && this.notificationType.special) { - return Type.SPECIAL_NOTIFICATIONS; - } else { - return type; - } + fun getCompareType(type: Type?): Type? { + return separator + ?: if (notificationType != null && notificationType.special) { + Type.SPECIAL_NOTIFICATIONS + } else { + type + } } - public boolean shouldRemove() { - if (this.repoModule != null && this.moduleInfo != null && !hasUpdate()) { - return true; + fun shouldRemove(): Boolean { + if (repoModule != null && moduleInfo != null && !hasUpdate()) { + 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 buttonTypeList, boolean showcaseMode) { - if (!this.isModuleHolder()) return; - LocalModuleInfo localModuleInfo = this.moduleInfo; + fun getButtons( + context: Context?, + buttonTypeList: MutableList, + 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 - if (this.moduleId.startsWith(".") || !this.moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) { - buttonTypeList.add(ActionButtonType.WARNING); + if (moduleId.startsWith(".") || !moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$".toRegex())) { + buttonTypeList.add(ActionButtonType.WARNING) } if (localModuleInfo != null && !showcaseMode) { - buttonTypeList.add(ActionButtonType.UNINSTALL); + buttonTypeList.add(ActionButtonType.UNINSTALL) } - if (this.repoModule != null && this.repoModule.notesUrl != null) { - buttonTypeList.add(ActionButtonType.INFO); + if (repoModule != null && repoModule!!.notesUrl != null) { + buttonTypeList.add(ActionButtonType.INFO) } - if ((this.repoModule != null || (localModuleInfo != null && localModuleInfo.updateZipUrl != null))) { - buttonTypeList.add(ActionButtonType.UPDATE_INSTALL); + if (repoModule != null || localModuleInfo?.updateZipUrl != null) { + buttonTypeList.add(ActionButtonType.UPDATE_INSTALL) } - String config = this.getMainModuleConfig(); + val config = mainModuleConfig if (config != null) { - if (config.startsWith("https://www.androidacy.com/") && Http.hasWebView()) { - buttonTypeList.add(ActionButtonType.CONFIG); + if (config.startsWith("https://www.androidacy.com/") && hasWebView()) { + buttonTypeList.add(ActionButtonType.CONFIG) } else { - String pkg = IntentHelper.getPackageOfConfig(config); + val pkg = getPackageOfConfig(config) try { - XHooks.checkConfigTargetExists(context, pkg, config); - buttonTypeList.add(ActionButtonType.CONFIG); - } catch (PackageManager.NameNotFoundException e) { - Timber.w("Config package \"" + pkg + "\" missing for module \"" + this.moduleId + "\""); + checkConfigTargetExists(context!!, pkg, config) + buttonTypeList.add(ActionButtonType.CONFIG) + } catch (e: PackageManager.NameNotFoundException) { + Timber.w("Config package \"$pkg\" missing for module \"$moduleId\"") } } } - ModuleInfo moduleInfo = this.getMainModuleInfo(); + var moduleInfo: ModuleInfo? = mainModuleInfo if (moduleInfo == null) { // Avoid concurrency NPE - if (localModuleInfo == null) return; - moduleInfo = localModuleInfo; + if (localModuleInfo == null) return + moduleInfo = localModuleInfo } if (moduleInfo.support != null) { - buttonTypeList.add(ActionButtonType.SUPPORT); + buttonTypeList.add(ActionButtonType.SUPPORT) } if (moduleInfo.donate != null) { - buttonTypeList.add(ActionButtonType.DONATE); + buttonTypeList.add(ActionButtonType.DONATE) } if (moduleInfo.safe) { - buttonTypeList.add(ActionButtonType.SAFE); + buttonTypeList.add(ActionButtonType.SAFE) } else { - Timber.d("Module %s is not safe", this.moduleId); + Timber.d("Module %s is not safe", moduleId) } } - public boolean hasUpdate() { - return this.moduleInfo != null && this.repoModule != null && this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode; + fun hasUpdate(): Boolean { + return moduleInfo != null && repoModule != null && moduleInfo!!.versionCode < repoModule!!.moduleInfo.versionCode } - @Override - public int compareTo(ModuleHolder o) { + override operator fun compareTo(other: ModuleHolder?): Int { // Compare depend on type, also allow type spoofing - Type selfTypeReal = this.getType(); - Type otherTypeReal = o.getType(); - Type selfType = this.getCompareType(selfTypeReal); - Type otherType = o.getCompareType(otherTypeReal); - int compare = selfType.compareTo(otherType); - return compare != 0 ? compare : selfTypeReal == otherTypeReal ? selfTypeReal.compare(this, o) : selfTypeReal.compareTo(otherTypeReal); + val selfTypeReal = type + val otherTypeReal = other!!.type + val selfType = getCompareType(selfTypeReal) + val otherType = other.getCompareType(otherTypeReal) + val compare = selfType!!.compareTo(otherType!!) + return if (compare != 0) compare else if (selfTypeReal === otherTypeReal) selfTypeReal.compare( + this, + other + ) else selfTypeReal.compareTo(otherTypeReal) } - @NonNull - @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 { - HEADER(R.string.loading, false, false), SEPARATOR(R.string.loading, false, false) { - @Override - @SuppressWarnings("ConstantConditions") - public int compare(ModuleHolder o1, ModuleHolder o2) { - return o1.separator.compareTo(o2.separator); + enum class Type( + @field:StringRes @param:StringRes val title: Int, + val hasBackground: Boolean, + val moduleHolder: Boolean + ) : Comparator { + HEADER(R.string.loading, false, false) { + override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int { + if (o1 != null && o2 != null) { + return o1.separator!!.compareTo(o2.separator!!) + } else if (o1 != null) { + return -1 + } else if (o2 != null) { + return 1 + } + return 0 } - }, NOTIFICATION(R.string.loading, true, false) { - @Override - @SuppressWarnings("ConstantConditions") - public int compare(ModuleHolder o1, ModuleHolder o2) { - return o1.notificationType.compareTo(o2.notificationType); + }, SEPARATOR(R.string.loading, false, false) { + override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int { + if (o1 != null && o2 != null) { + return o1.separator!!.compareTo(o2.separator!!) + } else if (o1 != null) { + return -1 + } else if (o2 != null) { + return 1 + } + return 0 } - }, UPDATABLE(R.string.updatable, true, true) { - @Override - public int compare(ModuleHolder o1, ModuleHolder o2) { - int cmp = Integer.compare(o1.filterLevel, o2.filterLevel); - if (cmp != 0) return cmp; - long lastUpdated1 = o1.repoModule == null ? 0L : o1.repoModule.lastUpdated; - long lastUpdated2 = o2.repoModule == null ? 0L : o2.repoModule.lastUpdated; - cmp = Long.compare(lastUpdated2, lastUpdated1); - if (cmp != 0) return cmp; - return o1.getMainModuleName().compareTo(o2.getMainModuleName()); + }, + 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 } - }, INSTALLED(R.string.installed, true, true) { + }, + 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 - @Override - public int compare(ModuleHolder o1, ModuleHolder o2) { - int cmp = Integer.compare(o1.filterLevel, o2.filterLevel); - if (cmp != 0) return cmp; - return o1.getMainModuleNameLowercase().compareTo(o2.getMainModuleNameLowercase()); + override fun compare(o1: ModuleHolder?, o2: ModuleHolder?): Int { + if (o1 != null && o2 != null) { + val cmp = o1.filterLevel.compareTo(o2.filterLevel) + return if (cmp != 0) cmp else o1.mainModuleNameLowercase.compareTo(o2.mainModuleNameLowercase) + } else if (o1 != null) { + return -1 + } else if (o2 != null) { + return 1 + } + return 0 } - }, SPECIAL_NOTIFICATIONS(R.string.loading, true, false), INSTALLABLE(R.string.online_repo, true, true) { - @Override - public int compare(ModuleHolder o1, ModuleHolder o2) { - int cmp = Integer.compare(o1.filterLevel, o2.filterLevel); - if (cmp != 0) return cmp; - long lastUpdated1 = o1.repoModule == null ? 0L : o1.repoModule.lastUpdated; - long lastUpdated2 = o2.repoModule == null ? 0L : o2.repoModule.lastUpdated; - cmp = Long.compare(lastUpdated2, lastUpdated1); - if (cmp != 0) return cmp; - return o1.getMainModuleName().compareTo(o2.getMainModuleName()); + }, + SPECIAL_NOTIFICATIONS(R.string.loading, true, false) { + 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 } - }, FOOTER(R.string.loading, false, false); - - @StringRes - public final int title; - public final boolean hasBackground; - public final boolean moduleHolder; - - Type(@StringRes int title, boolean hasBackground, boolean moduleHolder) { - this.title = title; - this.hasBackground = hasBackground; - this.moduleHolder = moduleHolder; - } - - // Note: This method should only be called if both element have the same type - @Override - public int compare(ModuleHolder o1, ModuleHolder o2) { - if (o1 == o2) { - return 0; - } else if (o1 == null) { - return -1; - } else if (o2 == null) { - return 1; - } else { - return o1.moduleId.compareTo(o2.moduleId); + }, INSTALLABLE( + R.string.online_repo, + 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 } - } + }, + 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 + } + }; } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewAdapter.kt b/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewAdapter.kt index 182e686..10d0e2a 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewAdapter.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewAdapter.kt @@ -56,8 +56,8 @@ class ModuleViewAdapter : RecyclerView.Adapter() { } } } - } catch (ignored: Exception) { - Timber.e("Error while updating module holder. This may mean we're trying to update too early.") + } catch (e: Exception) { + 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() { if (initState) return@setOnClickListener // Skip if non user val moduleHolder = moduleHolder 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()) { cardView.visibility = View.GONE } @@ -149,8 +149,10 @@ class ModuleViewAdapter : RecyclerView.Adapter() { val moduleHolder = moduleHolder var didSomething = false if (i < actionButtonsTypes.size && moduleHolder != null) { - didSomething = actionButtonsTypes[i]!! - .doActionLong(v as Chip?, moduleHolder) + didSomething = (v as Chip?)?.let { + actionButtonsTypes[i]!! + .doActionLong(it, moduleHolder) + } == true if (moduleHolder.shouldRemove()) { cardView.visibility = View.GONE } diff --git a/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewListBuilder.kt b/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewListBuilder.kt index cbaa376..54261fe 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewListBuilder.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/module/ModuleViewListBuilder.kt @@ -72,6 +72,8 @@ class ModuleViewListBuilder(private val activity: Activity) { Timber.i("appendRemoteModules() called") } synchronized(updateLock) { + Timber.i("appendRemoteModules() started") + val startTime = System.currentTimeMillis() val showIncompatible = MainApplication.isShowIncompatibleModules for (moduleHolder in mappedModuleHolders.values) { moduleHolder.repoModule = null @@ -119,6 +121,7 @@ class ModuleViewListBuilder(private val activity: Activity) { } } } + Timber.i("appendRemoteModules() finished in %dms", System.currentTimeMillis() - startTime) } } } diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt index bab26ff..cc8038a 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/CustomRepoManager.kt @@ -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.PropUtils.Companion.isNullString 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 org.json.JSONObject import timber.log.Timber import java.nio.charset.StandardCharsets -@Suppress("UNUSED_PARAMETER", "MemberVisibilityCanBePrivate") +@Suppress("MemberVisibilityCanBePrivate") class CustomRepoManager internal constructor( mainApplication: MainApplication?, private val repoManager: RepoManager ) { @@ -35,7 +36,7 @@ class CustomRepoManager internal constructor( // now the same as above but for room database val applicationContext = mainApplication!!.applicationContext val db = Room.databaseBuilder( - applicationContext, ReposListDatabase::class.java, "reposlist.db" + applicationContext, ReposListDatabase::class.java, "ReposList.db" ).build() val reposListDao = db.reposListDao() val reposListList = reposListDao.getAll() @@ -49,6 +50,7 @@ class CustomRepoManager internal constructor( (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" ).build() 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++ dirty = true val customRepoData = repoManager.addOrGet(repo) as CustomRepoData @@ -127,6 +130,7 @@ class CustomRepoManager internal constructor( // Set the enabled state to true customRepoData.isEnabled = true customRepoData.updateEnabledState() + db.close() return customRepoData } diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt index ff63269..255ed26 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoData.kt @@ -41,8 +41,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { private var metaDataCache: JSONObject? @JvmField - var lastUpdate - : Long = 0 + var lastUpdate: Long = 0 @JvmField var website: String? = null @@ -72,18 +71,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { var defaultSubmitModule: String? = null override var name: String? = null - get() = { - // 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() + get() = field ?: defaultName set(value) { field = value } @@ -150,9 +138,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { isForceHide = shouldForceHide(tempVarForPreferenceId) // basically same as above but for room database val db = Room.databaseBuilder( - INSTANCE!!.applicationContext, - ReposListDatabase::class.java, - "repo_database" + INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db" ).allowMainThreadQueries().build() val reposListRoom = db.reposListDao() 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.") } } + db.close() } open fun prepare(): Boolean { @@ -204,8 +191,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { var nameForModules = if (name.endsWith(" (Official)")) name.substring(0, name.length - 11) else name nameForModules = if (nameForModules.endsWith(" [Official]")) nameForModules.substring( - 0, - nameForModules.length - 11 + 0, nameForModules.length - 11 ) else nameForModules nameForModules = if (nameForModules.contains("Official")) nameForModules.replace("Official", "") @@ -307,9 +293,9 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { } else { val db = Room.databaseBuilder( INSTANCE!!.applicationContext, - ReposListDatabase::class.java, + ReposListDatabase::class.java, "ReposList.db", - ).build() + ).allowMainThreadQueries().build() val reposList = db.reposListDao().getById(preferenceId!!) // should never happen but for safety if (reposList.enabled) { @@ -325,9 +311,21 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db", - ).build() + ).allowMainThreadQueries().build() + 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) @@ -381,6 +379,7 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { ReposListDatabase::class.java, "ReposList.db", ).allowMainThreadQueries().build() + val reposList = db.reposListDao().getById(preferenceId!!) enabled = if (reposList.enabled) { !isForceHide @@ -433,10 +432,14 @@ open class RepoData(url: String, cacheRoot: File) : XRepo() { val diff = currentTime - lastUpdate val diffMinutes = diff / (60 * 1000) % 60 Timber.d("Repo $preferenceId updated: $diffMinutes minutes ago") + db.close() + db2.close() diffMinutes > if (BuildConfig.DEBUG) 15 else 30 } else { 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") + db.close() + db2.close() true } } else { diff --git a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoUpdater.kt b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoUpdater.kt index 51ff77e..0ead046 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/repo/RepoUpdater.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/repo/RepoUpdater.kt @@ -7,6 +7,7 @@ package com.fox2code.mmm.repo import androidx.room.Room import com.fox2code.mmm.MainApplication 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.ReposListDatabase import org.json.JSONArray @@ -127,169 +128,178 @@ class RepoUpdater(repoData2: RepoData) { return true } if (indexRaw != null) { - 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 - // use room to insert to - // props avail: - val db = Room.databaseBuilder( - MainApplication.INSTANCE!!, - ModuleListCacheDatabase::class.java, - "ModuleListCache.db" - ).allowMainThreadQueries().build() - // array with module info default values - // supported properties for a module - //id= - //name= - //version= - //versionCode= - //author= - //description= - //minApi= - //maxApi= - //minMagisk= - //needRamdisk= - //support= - //donate= - //config= - //changeBoot= - //mmtReborn= - // extra properties only useful for the database - //repoId= - //installed= - //installedVersionCode= (only if installed) - // - // all except first six can be null - // this.indexRaw is the raw index file (json) - val modules = JSONObject(String(indexRaw!!, StandardCharsets.UTF_8)) - // androidacy repo uses "data" key, others should use "modules" key. Both are JSONArrays - val modulesArray: JSONArray = if (repoData.name == "Androidacy Modules Repo") { - // get modules from "data" key. This is a JSONArray so we need to convert it to a JSONObject - modules.getJSONArray("data") - } else { - // get modules from "modules" key. This is a JSONArray so we need to convert it to a JSONObject - modules.getJSONArray("modules") - } - val moduleListCacheDao = db.moduleListCacheDao() - moduleListCacheDao.deleteByRepoId(repoData.preferenceId!!) - // iterate over modules. pls don't hate me for this, its ugly but it works - for (n in 0 until modulesArray.length()) { - // get module - val module = modulesArray.getJSONObject(n) - try { - // get module id - // if codename is present, prefer that over id - val id: String? = - if (module.has("codename") && module.getString("codename") != "") { - module.getString("codename") + // new thread to update the database + val thread = Thread { + val startTime = System.currentTimeMillis() + Timber.d("Updating database for %s", repoData.preferenceId) + 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 + // use room to insert to + // props avail: + val db = Room.databaseBuilder( + MainApplication.INSTANCE!!, + ModuleListCacheDatabase::class.java, + "ModuleListCache.db" + ).build() + // all except first six can be null + // this.indexRaw is the raw index file (json) + val modules = JSONObject(String(indexRaw!!, StandardCharsets.UTF_8)) + // androidacy repo uses "data" key, others should use "modules" key. Both are JSONArrays + val modulesArray: JSONArray = if (repoData.name == "Androidacy Modules Repo") { + // get modules from "data" key. This is a JSONArray so we need to convert it to a JSONObject + modules.getJSONArray("data") + } else { + // get modules from "modules" key. This is a JSONArray so we need to convert it to a JSONObject + modules.getJSONArray("modules") + } + val moduleListCacheDao = db.moduleListCacheDao() + moduleListCacheDao.deleteByRepoId(repoData.preferenceId!!) + // iterate over modules. pls don't hate me for this, its ugly but it works + for (n in 0 until modulesArray.length()) { + // get module + val module = modulesArray.getJSONObject(n) + try { + // get module id + // if codename is present, prefer that over id + val id: String? = + if (module.has("codename") && module.getString("codename") != "") { + module.getString("codename") + } else { + module.getString("id") + } + // get module name + val name = module.getString("name") + // get module version + val version = module.getString("version") + // get module version code + val versionCode = module.getInt("versionCode") + // get module author + val author = module.getString("author") + // get module description + val description = module.getString("description") + // get module min api + val minApi: String = + if (module.has("minApi") && module.getString("minApi") != "") { + module.getString("minApi") + } else { + "0" + } + // coerce min api to int + val minApiInt = minApi.toInt() + // get module max api and set to 0 if it's "" or null + val maxApi: String = + if (module.has("maxApi") && module.getString("maxApi") != "") { + module.getString("maxApi") + } else { + "0" + } + // coerce max api to int + val maxApiInt = maxApi.toInt() + // get module min magisk + val minMagisk: String = + if (module.has("minMagisk") && module.getString("minMagisk") != "") { + module.getString("minMagisk") + } else { + "0" + } + // coerce min magisk to int + val minMagiskInt = minMagisk.toInt() + // get module need ramdisk + val needRamdisk: Boolean = if (module.has("needRamdisk")) { + module.getBoolean("needRamdisk") } else { - module.getString("id") + false } - // get module name - val name = module.getString("name") - // get module version - val version = module.getString("version") - // get module version code - val versionCode = module.getInt("versionCode") - // get module author - val author = module.getString("author") - // get module description - val description = module.getString("description") - // get module min api - val minApi: String = - if (module.has("minApi") && module.getString("minApi") != "") { - module.getString("minApi") + // get module support + val support: String? = if (module.has("support")) { + module.getString("support") } else { - "0" + "" } - // coerce min api to int - val minApiInt = minApi.toInt() - // get module max api and set to 0 if it's "" or null - val maxApi: String = - if (module.has("maxApi") && module.getString("maxApi") != "") { - module.getString("maxApi") + // get module donate + val donate: String? = if (module.has("donate")) { + module.getString("donate") } else { - "0" + "" } - // coerce max api to int - val maxApiInt = maxApi.toInt() - // get module min magisk - val minMagisk: String = - if (module.has("minMagisk") && module.getString("minMagisk") != "") { - module.getString("minMagisk") + // get module config + val config: String? = if (module.has("config")) { + module.getString("config") } else { - "0" + "" } - // coerce min magisk to int - val minMagiskInt = minMagisk.toInt() - // get module need ramdisk - val needRamdisk: Boolean = if (module.has("needRamdisk")) { - module.getBoolean("needRamdisk") - } else { - false - } - // get module support - val support: String? = if (module.has("support")) { - module.getString("support") - } else { - "" - } - // get module donate - val donate: String? = if (module.has("donate")) { - module.getString("donate") - } else { - "" - } - // get module config - val config: String? = if (module.has("config")) { - module.getString("config") - } else { - "" - } - // get module change boot - val changeBoot: Boolean = if (module.has("changeBoot")) { - module.getBoolean("changeBoot") - } else { - false - } - // get module mmt reborn - val mmtReborn: Boolean = if (module.has("mmtReborn")) { - module.getBoolean("mmtReborn") - } else { - false - } - // try to get updated_at or lastUpdate value for lastUpdate - val lastUpdate: Int = if (module.has("updated_at")) { - module.getInt("updated_at") - } else if (module.has("lastUpdate")) { - module.getInt("lastUpdate") - } else { - 0 - } - // now downloads or stars - val downloads: Int = if (module.has("downloads")) { - module.getInt("downloads") - } else if (module.has("stars")) { - module.getInt("stars") - } else { - 0 - } - // get module repo id - val repoId = repoData.preferenceId - // get safe property. for now, only supported by androidacy repo and they use "vt_status" key - var safe = false - if (repoData.name == "Androidacy Modules Repo") { - if (module.has("vt_status")) { - if (module.getString("vt_status") == "Clean") { - safe = true + // get module change boot + val changeBoot: Boolean = if (module.has("changeBoot")) { + module.getBoolean("changeBoot") + } else { + false + } + // get module mmt reborn + val mmtReborn: Boolean = if (module.has("mmtReborn")) { + module.getBoolean("mmtReborn") + } else { + false + } + // try to get updated_at or lastUpdate value for lastUpdate + val lastUpdate: Int = if (module.has("updated_at")) { + module.getInt("updated_at") + } else if (module.has("lastUpdate")) { + module.getInt("lastUpdate") + } else { + 0 + } + // now downloads or stars + val downloads: Int = if (module.has("downloads")) { + module.getInt("downloads") + } else if (module.has("stars")) { + module.getInt("stars") + } else { + 0 + } + // get module repo id + val repoId = repoData.preferenceId + // get safe property. for now, only supported by androidacy repo and they use "vt_status" key + var safe = false + if (repoData.name == "Androidacy Modules Repo") { + if (module.has("vt_status")) { + if (module.getString("vt_status") == "Clean") { + safe = true + } } } + 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) { } - 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 ?: "") - } 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 // set lastUpdate val db = Room.databaseBuilder( @@ -300,6 +310,7 @@ class RepoUpdater(repoData2: RepoData) { val repoListDao = db.reposListDao() repoListDao.setLastUpdate(repoData.preferenceId!!, System.currentTimeMillis()) db.close() + success.set(true) } else { success.set(true) // assume we're reading from cache. this may be unsafe but it's better than nothing } diff --git a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java index 81c162c..0095679 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/kotlin/com/fox2code/mmm/settings/SettingsActivity.java @@ -5,7 +5,6 @@ package com.fox2code.mmm.settings; import static com.fox2code.mmm.settings.SettingsActivity.RepoFragment.applyMaterial3; -import static java.lang.Integer.parseInt; import android.annotation.SuppressLint; import android.app.ActivityManager; @@ -98,12 +97,12 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.io.RandomAccessFile; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.Locale; +import java.util.List; import java.util.Objects; import java.util.Random; import java.util.Set; @@ -136,35 +135,30 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { @PerformanceClass public static int getDevicePerformanceClass() { - int devicePerformanceClass; - int androidVersion = Build.VERSION.SDK_INT; - 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); + // 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 + // device is awarded 1 point for each core and 1 point for each GB of ram. - 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) { - devicePerformanceClass = PERFORMANCE_CLASS_LOW; - } else if (cpuCount < 8 || memoryClass <= 160 || maxCpuFreq != -1 && maxCpuFreq <= 2050 || maxCpuFreq == -1 && cpuCount == 8 && androidVersion <= 23) { - devicePerformanceClass = PERFORMANCE_CLASS_AVERAGE; + int points = 0; + int cores = Runtime.getRuntime().availableProcessors(); + ActivityManager activityManager = (ActivityManager) Objects.requireNonNull(MainApplication.getINSTANCE()).getSystemService(Context.ACTIVITY_SERVICE); + 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 { - 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") @@ -1072,6 +1066,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } // Get magisk_alt_repo enabled state from room reposlist db ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); + // add listener to magisk_alt_repo_enabled switch to update room db Preference magiskAltRepoEnabled = Objects.requireNonNull(findPreference("pref_magisk_alt_repo_enabled")); magiskAltRepoEnabled.setOnPreferenceChangeListener((preference, newValue) -> { @@ -1259,17 +1254,17 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { @SuppressLint("RestrictedApi") 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 int CUSTOM_REPO_ENTRIES = 0; // array of custom repos ArrayList customRepos = new ArrayList<>(); - RealmResults customRepoDataDB = realm.where(ReposList.class).findAll(); - for (ReposList repo : customRepoDataDB) { - if (!repo.getId().equals("androidacy_repo") && !repo.getId().equals("magisk_alt_repo")) { + ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); + List reposList = db.reposListDao().getAll(); + for (ReposList repo : reposList) { + ArrayList buildInRepos = new ArrayList<>(Arrays.asList("androidacy_repo", "magisk_alt_repo")); + if (!buildInRepos.contains(repo.getId())) { CUSTOM_REPO_ENTRIES++; - customRepos.add(repo.getUrl()); + customRepos.add(repo.getId()); } } Timber.d("%d repos: %s", CUSTOM_REPO_ENTRIES, customRepos); @@ -1286,12 +1281,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { if (preference == null) continue; final int index = i; preference.setOnPreferenceClickListener(preference1 -> { - if (realm.isInTransaction()) { - realm.commitTransaction(); - } - realm.beginTransaction(); - Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findFirst()).deleteFromRealm(); - realm.commitTransaction(); + db.reposListDao().delete(customRepos.get(index)); customRepoManager.removeRepo(index); updateCustomRepoList(false); preference1.setVisible(false); @@ -1428,16 +1418,15 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { if (preference == null) return; if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) { 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(); - Realm realm = Realm.getInstance(realmConfiguration); - RealmResults repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findAll(); + ReposListDatabase db = Room.databaseBuilder(requireContext(), ReposListDatabase.class, "ReposList.db").allowMainThreadQueries().build(); + ReposList reposList = db.reposListDao().getById(repoData.preferenceId); Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); - if (repoData.isForceHide() || repoDataRealmResults.isEmpty()) { + if (repoData.isForceHide() || reposList == null) { Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden"); hideRepoData(preferenceName); return; } else { - Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), 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.setVisible(true); // 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); } } - realm.close(); } else { Timber.d("Hiding preference " + preferenceName + " because it's data is null"); hideRepoData(preferenceName); diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/io/PropUtils.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/io/PropUtils.kt index fb1de2e..5625ba4 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/io/PropUtils.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/io/PropUtils.kt @@ -382,10 +382,15 @@ enum class PropUtils { InputStreamReader(inputStream, StandardCharsets.UTF_8) ).use { bufferedReader -> 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) if (line.startsWith("$what=")) { moduleId = line.substring(what.length + 1).trim { it <= ' ' } + break } } } diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCache.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCache.kt index bbd76de..d703fa8 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCache.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCache.kt @@ -7,7 +7,7 @@ package com.fox2code.mmm.utils.room import androidx.room.Entity @Suppress("unused") -@Entity(tableName = "modulelistcache") +@Entity(tableName = "modulelistcache", primaryKeys = ["codename"], indices = [androidx.room.Index(value = ["codename"], unique = true)]) class ModuleListCache ( var codename: String, var version: String, diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCacheDao.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCacheDao.kt index 7026aac..d8adc9e 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCacheDao.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ModuleListCacheDao.kt @@ -69,7 +69,7 @@ interface ModuleListCacheDao { fun getByCodename(codename: String): ModuleListCache @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") fun setVersion(codename: String, version: String) diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposList.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposList.kt index 8405d8a..2d61064 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposList.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposList.kt @@ -5,11 +5,10 @@ package com.fox2code.mmm.utils.room 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( - @PrimaryKey var id: String, + var id: String, var url: String, var enabled: Boolean, var donate: String?, diff --git a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposListDao.kt b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposListDao.kt index e12d59f..83c972d 100644 --- a/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposListDao.kt +++ b/app/src/main/kotlin/com/fox2code/mmm/utils/room/ReposListDao.kt @@ -5,6 +5,8 @@ package com.fox2code.mmm.utils.room import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query @Suppress("unused") @@ -26,11 +28,11 @@ interface ReposListDao { @Query("SELECT * FROM ReposList WHERE id = :id") fun getById(id: String): ReposList - @Query("INSERT INTO ReposList VALUES (:id, :url, :enabled, :donate, :support, :submitModule, :lastUpdate, :name, :website)") - fun insert(id: String, url: String, enabled: Boolean, donate: String?, support: String?, submitModule: String?, lastUpdate: Long, name: String, website: String?) + @Insert(entity = ReposList::class, onConflict = OnConflictStrategy.REPLACE) + 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") - 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") fun delete(id: String) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 22e86cb..57fc253 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -439,4 +439,6 @@ Most modules should be uninstalled, then reinstalled cleanly. For this reason, AMM does not support reinstalling modules while they are installed on the device. This build has expired. Please update to the latest version. EULA and Terms + Could not create repos db + Failed to create module cache db diff --git a/build.gradle.kts b/build.gradle.kts index b9c9e33..9eeff84 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,8 +5,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { - mavenCentral() google() + mavenCentral() maven { setUrl("https://jitpack.io") }