|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
@file:Suppress("unused")
|
|
|
|
|
|
|
|
package com.fox2code.mmm.manager
|
|
|
|
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import com.fox2code.mmm.BuildConfig
|
|
|
|
import com.fox2code.mmm.MainApplication
|
|
|
|
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekModulesPath
|
|
|
|
import com.fox2code.mmm.utils.SyncManager
|
|
|
|
import com.fox2code.mmm.utils.io.PropUtils
|
|
|
|
import com.fox2code.mmm.utils.realm.ModuleListCache
|
|
|
|
import com.topjohnwu.superuser.Shell
|
|
|
|
import com.topjohnwu.superuser.io.SuFile
|
|
|
|
import com.topjohnwu.superuser.io.SuFileInputStream
|
|
|
|
import io.realm.Realm
|
|
|
|
import io.realm.RealmConfiguration
|
|
|
|
import org.matomo.sdk.extra.TrackHelper
|
|
|
|
import timber.log.Timber
|
|
|
|
import java.io.BufferedReader
|
|
|
|
import java.io.File
|
|
|
|
import java.io.IOException
|
|
|
|
import java.io.InputStreamReader
|
|
|
|
import java.nio.charset.StandardCharsets
|
|
|
|
import java.util.Objects
|
|
|
|
|
|
|
|
class ModuleManager private constructor() : SyncManager() {
|
|
|
|
private val moduleInfos: HashMap<String, LocalModuleInfo> = HashMap()
|
|
|
|
private val bootPrefs: SharedPreferences = MainApplication.bootSharedPreferences!!
|
|
|
|
private var updatableModuleCount = 0
|
|
|
|
|
|
|
|
override fun scanInternal(updateListener: UpdateListener) {
|
|
|
|
// if last_shown_setup is not "v2", then refuse to continue
|
|
|
|
if (MainApplication.getSharedPreferences("mmm")!!.getString("last_shown_setup", "") != "v2") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val firstScan = bootPrefs.getBoolean("mm_first_scan", true)
|
|
|
|
val editor = if (firstScan) bootPrefs.edit() else null
|
|
|
|
for (v in moduleInfos.values) {
|
|
|
|
v.flags = v.flags or FLAG_MM_UNPROCESSED
|
|
|
|
v.flags = v.flags and FLAGS_KEEP_INIT
|
|
|
|
v.name = v.id
|
|
|
|
v.version = null
|
|
|
|
v.versionCode = 0
|
|
|
|
v.author = null
|
|
|
|
v.description = ""
|
|
|
|
v.support = null
|
|
|
|
v.config = null
|
|
|
|
}
|
|
|
|
val modulesPath = peekModulesPath()
|
|
|
|
val modules = SuFile("/data/adb/modules").list()
|
|
|
|
val needFallback =
|
|
|
|
FORCE_NEED_FALLBACK || modulesPath == null || !SuFile(modulesPath).exists()
|
|
|
|
if (!FORCE_NEED_FALLBACK && needFallback) {
|
|
|
|
Timber.e("using fallback instead.")
|
|
|
|
}
|
|
|
|
if (BuildConfig.DEBUG) Timber.d("Scan")
|
|
|
|
val modulesList = StringBuilder()
|
|
|
|
if (modules != null) {
|
|
|
|
for (module in modules) {
|
|
|
|
if (!SuFile("/data/adb/modules/$module").isDirectory) continue // Ignore non directory files inside modules folder
|
|
|
|
var moduleInfo = moduleInfos[module]
|
|
|
|
// next, merge the module info with a record from ModuleListCache if it exists
|
|
|
|
var realmConfiguration: RealmConfiguration?
|
|
|
|
// get all dirs under the realms/repos/ dir under app's data dir
|
|
|
|
val cacheRoot =
|
|
|
|
File(MainApplication.INSTANCE!!.getDataDirWithPath("realms/repos/").toURI())
|
|
|
|
var moduleListCache: ModuleListCache?
|
|
|
|
for (dir in Objects.requireNonNull<Array<File>>(cacheRoot.listFiles())) {
|
|
|
|
if (dir.isDirectory) {
|
|
|
|
// if the dir name matches the module name, use it as the cache dir
|
|
|
|
val tempCacheRoot = File(dir.toString())
|
|
|
|
Timber.d("Looking for cache in %s", tempCacheRoot)
|
|
|
|
realmConfiguration =
|
|
|
|
RealmConfiguration.Builder().name("ModuleListCache.realm")
|
|
|
|
.encryptionKey(MainApplication.INSTANCE!!.key).schemaVersion(1)
|
|
|
|
.deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true)
|
|
|
|
.allowQueriesOnUiThread(true).directory(tempCacheRoot).build()
|
|
|
|
val realm = Realm.getInstance(realmConfiguration!!)
|
|
|
|
Timber.d(
|
|
|
|
"Looking for cache for %s out of %d", module, realm.where(
|
|
|
|
ModuleListCache::class.java
|
|
|
|
).count()
|
|
|
|
)
|
|
|
|
moduleListCache =
|
|
|
|
realm.where(ModuleListCache::class.java).equalTo("codename", module)
|
|
|
|
.findFirst()
|
|
|
|
Timber.d("Found cache for %s", module)
|
|
|
|
// get module info from cache
|
|
|
|
if (moduleInfo == null) {
|
|
|
|
moduleInfo = LocalModuleInfo(module)
|
|
|
|
}
|
|
|
|
if (moduleListCache != null) {
|
|
|
|
moduleInfo.name =
|
|
|
|
if (moduleListCache.name != "") moduleListCache.name else module
|
|
|
|
moduleInfo.description =
|
|
|
|
if (moduleListCache.description != "") moduleListCache.description else moduleInfo.description
|
|
|
|
moduleInfo.author =
|
|
|
|
if (moduleListCache.author != "") moduleListCache.author else moduleInfo.author
|
|
|
|
moduleInfo.safe = moduleListCache.isSafe == true
|
|
|
|
moduleInfo.support =
|
|
|
|
if (moduleListCache.support != "") moduleListCache.support else null
|
|
|
|
moduleInfo.donate =
|
|
|
|
if (moduleListCache.donate != "") moduleListCache.donate else null
|
|
|
|
moduleInfo.flags = moduleInfo.flags or FLAG_MM_REMOTE_MODULE
|
|
|
|
moduleInfos[module] = moduleInfo
|
|
|
|
}
|
|
|
|
realm.close()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (moduleInfo == null) {
|
|
|
|
moduleInfo = LocalModuleInfo(module)
|
|
|
|
moduleInfos[module] = moduleInfo
|
|
|
|
// This should not really happen, but let's handles theses cases anyway
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_UPDATING_ONLY
|
|
|
|
}
|
|
|
|
moduleInfo.flags = moduleInfo.flags and FLAGS_RESET_UPDATE.inv()
|
|
|
|
if (SuFile("/data/adb/modules/$module/disable").exists()) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_DISABLED
|
|
|
|
} else if (firstScan && needFallback) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_ACTIVE
|
|
|
|
editor!!.putBoolean("module_" + moduleInfo.id + "_active", true)
|
|
|
|
}
|
|
|
|
if (SuFile("/data/adb/modules/$module/remove").exists()) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_UNINSTALLING
|
|
|
|
}
|
|
|
|
if (firstScan && !needFallback && SuFile(
|
|
|
|
modulesPath,
|
|
|
|
module
|
|
|
|
).exists() || bootPrefs.getBoolean("module_" + moduleInfo.id + "_active", false)
|
|
|
|
) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_ACTIVE
|
|
|
|
if (firstScan) {
|
|
|
|
editor!!.putBoolean("module_" + moduleInfo.id + "_active", true)
|
|
|
|
}
|
|
|
|
} else if (!needFallback) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags and ModuleInfo.FLAG_MODULE_ACTIVE.inv()
|
|
|
|
}
|
|
|
|
if (moduleInfo.flags and ModuleInfo.FLAGS_MODULE_ACTIVE != 0 && (SuFile(
|
|
|
|
"/data/adb/modules/$module/system"
|
|
|
|
).exists() || SuFile("/data/adb/modules/$module/vendor").exists() || SuFile("/data/adb/modules/$module/zygisk").exists() || SuFile(
|
|
|
|
"/data/adb/modules/$module/riru"
|
|
|
|
).exists())
|
|
|
|
) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
PropUtils.readProperties(
|
|
|
|
moduleInfo,
|
|
|
|
"/data/adb/modules/$module/module.prop",
|
|
|
|
true
|
|
|
|
)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
if (BuildConfig.DEBUG) Timber.d(e)
|
|
|
|
moduleInfo.flags = moduleInfo.flags or FLAG_MM_INVALID
|
|
|
|
}
|
|
|
|
// append moduleID:moduleName to the list
|
|
|
|
modulesList.append(moduleInfo.id).append(":").append(moduleInfo.versionCode)
|
|
|
|
.append(",")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (modulesList.isNotEmpty()) {
|
|
|
|
modulesList.deleteCharAt(modulesList.length - 1)
|
|
|
|
}
|
|
|
|
// send list to matomo
|
|
|
|
TrackHelper.track().event("installed_modules", modulesList.toString())
|
|
|
|
.with(MainApplication.INSTANCE!!.tracker)
|
|
|
|
if (BuildConfig.DEBUG) Timber.d("Scan update")
|
|
|
|
val modulesUpdate = SuFile("/data/adb/modules_update").list()
|
|
|
|
if (modulesUpdate != null) {
|
|
|
|
for (module in modulesUpdate) {
|
|
|
|
if (!SuFile("/data/adb/modules_update/$module").isDirectory) continue // Ignore non directory files inside modules folder
|
|
|
|
if (BuildConfig.DEBUG) Timber.d(module)
|
|
|
|
var moduleInfo = moduleInfos[module]
|
|
|
|
if (moduleInfo == null) {
|
|
|
|
moduleInfo = LocalModuleInfo(module)
|
|
|
|
moduleInfos[module] = moduleInfo
|
|
|
|
}
|
|
|
|
moduleInfo.flags = moduleInfo.flags and FLAGS_RESET_UPDATE.inv()
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_UPDATING
|
|
|
|
try {
|
|
|
|
PropUtils.readProperties(
|
|
|
|
moduleInfo,
|
|
|
|
"/data/adb/modules_update/$module/module.prop",
|
|
|
|
true
|
|
|
|
)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
if (BuildConfig.DEBUG) Timber.d(e)
|
|
|
|
moduleInfo.flags = moduleInfo.flags or FLAG_MM_INVALID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (BuildConfig.DEBUG) Timber.d("Finalize scan")
|
|
|
|
updatableModuleCount = 0
|
|
|
|
val moduleInfoIterator = moduleInfos.values.iterator()
|
|
|
|
while (moduleInfoIterator.hasNext()) {
|
|
|
|
val moduleInfo = moduleInfoIterator.next()
|
|
|
|
if (BuildConfig.DEBUG) Timber.d(moduleInfo.id)
|
|
|
|
if (moduleInfo.flags and FLAG_MM_UNPROCESSED != 0) {
|
|
|
|
moduleInfoIterator.remove()
|
|
|
|
continue // Don't process fallbacks if unreferenced
|
|
|
|
}
|
|
|
|
if (moduleInfo.updateJson != null && moduleInfo.flags and FLAG_MM_REMOTE_MODULE == 0) {
|
|
|
|
updatableModuleCount++
|
|
|
|
} else {
|
|
|
|
moduleInfo.updateVersion = null
|
|
|
|
moduleInfo.updateVersionCode = Long.MIN_VALUE
|
|
|
|
moduleInfo.updateZipUrl = null
|
|
|
|
moduleInfo.updateChangeLog = ""
|
|
|
|
}
|
|
|
|
if (moduleInfo.name == null || moduleInfo.name == moduleInfo.id) {
|
|
|
|
moduleInfo.name =
|
|
|
|
moduleInfo.id[0].uppercaseChar().toString() + moduleInfo.id.substring(1)
|
|
|
|
.replace('_', ' ')
|
|
|
|
}
|
|
|
|
if (moduleInfo.version == null || moduleInfo.version!!.trim { it <= ' ' }.isEmpty()) {
|
|
|
|
moduleInfo.version = "v" + moduleInfo.versionCode
|
|
|
|
}
|
|
|
|
moduleInfo.verify()
|
|
|
|
}
|
|
|
|
if (firstScan) {
|
|
|
|
editor!!.putBoolean("mm_first_scan", false)
|
|
|
|
editor.apply()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val modules: HashMap<String, LocalModuleInfo>
|
|
|
|
get() {
|
|
|
|
afterScan()
|
|
|
|
return moduleInfos
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("unused")
|
|
|
|
fun getUpdatableModuleCount(): Int {
|
|
|
|
afterScan()
|
|
|
|
return updatableModuleCount
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("unused")
|
|
|
|
fun setEnabledState(moduleInfo: ModuleInfo, checked: Boolean): Boolean {
|
|
|
|
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false
|
|
|
|
val disable = SuFile("/data/adb/modules/" + moduleInfo.id + "/disable")
|
|
|
|
if (checked) {
|
|
|
|
if (disable.exists() && !disable.delete()) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_DISABLED
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
moduleInfo.flags = moduleInfo.flags and ModuleInfo.FLAG_MODULE_DISABLED.inv()
|
|
|
|
} else {
|
|
|
|
if (!disable.exists() && !disable.createNewFile()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_DISABLED
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("unused")
|
|
|
|
fun setUninstallState(moduleInfo: ModuleInfo, checked: Boolean): Boolean {
|
|
|
|
if (checked && moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) return false
|
|
|
|
val disable = SuFile("/data/adb/modules/" + moduleInfo.id + "/remove")
|
|
|
|
if (checked) {
|
|
|
|
if (!disable.exists() && !disable.createNewFile()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_UNINSTALLING
|
|
|
|
} else {
|
|
|
|
if (disable.exists() && !disable.delete()) {
|
|
|
|
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_MODULE_UNINSTALLING
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
moduleInfo.flags = moduleInfo.flags and ModuleInfo.FLAG_MODULE_UNINSTALLING.inv()
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
fun masterClear(moduleInfo: ModuleInfo): Boolean {
|
|
|
|
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) return false
|
|
|
|
val escapedId =
|
|
|
|
moduleInfo.id.replace("\\", "\\\\").replace("\"", "\\\"").replace(" ", "\\ ")
|
|
|
|
try { // Check for module that declare having file outside their own folder.
|
|
|
|
BufferedReader(
|
|
|
|
InputStreamReader(
|
|
|
|
SuFileInputStream.open("/data/adb/modules/." + moduleInfo.id + "-files"),
|
|
|
|
StandardCharsets.UTF_8
|
|
|
|
)
|
|
|
|
).use { bufferedReader ->
|
|
|
|
var line: String
|
|
|
|
while (bufferedReader.readLine().also { line = it } != null) {
|
|
|
|
line = line.trim { it <= ' ' }.replace(' ', '.')
|
|
|
|
if (!line.startsWith("/data/adb/") || line.contains("*") || line.contains("/../") || line.endsWith(
|
|
|
|
"/.."
|
|
|
|
) || line.startsWith("/data/adb/modules") || line == "/data/adb/magisk.db"
|
|
|
|
) continue
|
|
|
|
line = line.replace("\\", "\\\\").replace("\"", "\\\"")
|
|
|
|
Shell.cmd("rm -rf \"$line\"").exec()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (ignored: IOException) {
|
|
|
|
}
|
|
|
|
Shell.cmd("rm -rf /data/adb/modules/$escapedId/").exec()
|
|
|
|
Shell.cmd("rm -f /data/adb/modules/.$escapedId-files").exec()
|
|
|
|
Shell.cmd("rm -rf /data/adb/modules_update/$escapedId/").exec()
|
|
|
|
moduleInfo.flags = ModuleInfo.FLAG_METADATA_INVALID
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
// New method is not really effective, this flag force app to use old method
|
|
|
|
const val FORCE_NEED_FALLBACK = true
|
|
|
|
private const val FLAG_MM_INVALID = ModuleInfo.FLAG_METADATA_INVALID
|
|
|
|
private const val FLAG_MM_UNPROCESSED = ModuleInfo.FLAG_CUSTOM_INTERNAL
|
|
|
|
private const val FLAGS_KEEP_INIT =
|
|
|
|
FLAG_MM_UNPROCESSED or ModuleInfo.FLAGS_MODULE_ACTIVE or ModuleInfo.FLAG_MODULE_UPDATING_ONLY
|
|
|
|
private const val FLAGS_RESET_UPDATE = FLAG_MM_INVALID or FLAG_MM_UNPROCESSED
|
|
|
|
private var instAnce: ModuleManager? = null
|
|
|
|
@JvmStatic
|
|
|
|
val instance: ModuleManager?
|
|
|
|
get() {
|
|
|
|
if (instAnce == null) {
|
|
|
|
instAnce = ModuleManager()
|
|
|
|
}
|
|
|
|
return instAnce
|
|
|
|
}
|
|
|
|
private const val FLAG_MM_REMOTE_MODULE = ModuleInfo.FLAG_MM_REMOTE_MODULE
|
|
|
|
fun isModuleActive(moduleId: String): Boolean {
|
|
|
|
val moduleInfo: ModuleInfo? = instance!!.modules[moduleId]
|
|
|
|
return moduleInfo != null && moduleInfo.flags and ModuleInfo.FLAGS_MODULE_ACTIVE != 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|