You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MagiskModuleManager/app/src/main/kotlin/com/fox2code/mmm/manager/ModuleManager.kt

312 lines
14 KiB
Kotlin

/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
@file:Suppress("unused")
package com.fox2code.mmm.manager
import android.content.SharedPreferences
import androidx.room.Room
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.room.ModuleListCache
import com.fox2code.mmm.utils.room.ModuleListCacheDao
import com.fox2code.mmm.utils.room.ModuleListCacheDatabase
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
class ModuleManager private constructor() : SyncManager() {
private var 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 "v3", then refuse to continue
if (MainApplication.getSharedPreferences("mmm")!!
.getString("last_shown_setup", "") != "v3"
) {
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) {
val db = Room.databaseBuilder(
MainApplication.INSTANCE!!,
ModuleListCacheDatabase::class.java,
"ModuleListCache.db"
).allowMainThreadQueries().build()
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 room db if it exists
// initialize modulelistcache db
// get module info from cache
val moduleListCacheDao: ModuleListCacheDao = db.moduleListCacheDao()
// get module info from cache
if (moduleInfo == null) {
moduleInfo = LocalModuleInfo(module)
}
if (moduleListCacheDao.exists(module)) {
Timber.d("Found cache for %s", module)
val moduleListCache: ModuleListCache = moduleListCacheDao.getByCodename(module)
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.safe == 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
}
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(",")
}
db.close()
}
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()
}
}
var modules: HashMap<String, LocalModuleInfo> = HashMap()
get() {
afterScan()
return field
}
set(value) {
field = value
moduleInfos = value
}
@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
}
}
}