diff --git a/README.md b/README.md
index fed0b6a1d..984efa9d0 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,7 @@ This project is made possible by the following open-source contributions:
* [XposedBridge](https://github.com/rovo89/XposedBridge): The standard Xposed APIs.
* [Dobby](https://github.com/JingMatrix/Dobby): Inline hooking implementation.
* [LSPosed](https://github.com/LSPosed/LSPosed): Upstream source.
+* [EdXposed](https://github.com/ElderDrivers/EdXposed): Upstream source, before LSPosed.
* [xz-embedded](https://github.com/tukaani-project/xz-embedded): Library decompression utilities.
diff --git a/daemon/README.md b/daemon/README.md
index c913307e2..ab1946f44 100644
--- a/daemon/README.md
+++ b/daemon/README.md
@@ -14,7 +14,7 @@ src/main/
└── kotlin/org/matrix/vector/daemon/
├── data/ # SQLite schema, immutable state cache, and file operations
├── env/ # UNIX domain socket servers and native process monitors
- ├── ipc/ # AIDL endpoints (Application, Manager, Module, SystemServer)
+ ├── ipc/ # AIDL endpoints (Framework, Manager, ModuleApp, InjectedModule, SystemServer)
├── system/ # System binder delegates and Notification UI
├── utils/ # Context forgery, signature verification, and JNI bridges
├── Cli.kt # Command-line interface definitions
@@ -47,15 +47,15 @@ When a standard user application spawns, it requests framework access from the d
* The target application queries the `activity` service. The Zygisk module inside `system_server` intercepts this query.
* The `system_server` forwards the application's UID, PID, process name, and a newly created heartbeat `BBinder` to the daemon using the previously stored `VectorService` reference.
* The daemon verifies the request against its `ConfigCache` to determine if the application is within the scope of any enabled modules.
-* If approved, the daemon returns an `ApplicationService` binder, which the `system_server` passes back to the target application.
+* If approved, the daemon returns an `FrameworkService` binder, which the `system_server` passes back to the target application.
* The daemon links a `DeathRecipient` to the heartbeat binder to automatically clean up internal tracking maps when the application process dies.
-* The target application uses the `ApplicationService` binder to fetch its specific module list, framework DEX, and obfuscation map.
+* The target application uses the `FrameworkService` binder to fetch its specific module list, framework DEX, and obfuscation map.
### 3. Libxposed Module Injection
Unlike target applications which request access, the daemon actively pushes its API binder to module processes. This mechanism is strictly limited to modules utilizing the modern libxposed API.
* The daemon registers an `IUidObserver` with the Activity Manager to monitor process lifecycles.
-* When a UID becomes active, `ModuleService` checks if the UID belongs to an enabled libxposed module.
+* When a UID becomes active, `ModuleAppService` checks if the UID belongs to an enabled libxposed module.
* The daemon retrieves an `IXposedService` binder. To deliver it, the daemon calls `IActivityManager.getContentProviderExternal`, targeting a synthetic authority constructed from the module's package name.
* The daemon executes `IContentProvider.call` with the action `SEND_BINDER` and a `Bundle` containing the binder. This injects the binder into the module's process space before `Application.onCreate` executes, providing access to API verification, scope requests, and remote preferences.
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt
index a53862c91..b4fad62af 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt
@@ -79,7 +79,7 @@ object VectorDaemon {
@Suppress("DEPRECATION") Looper.prepareMainLooper()
// Squat on the proxy service name immediately, which creates the early IPC channel of
- // ApplicationService for our Zygisk module during system_server specialization.
+ // FrameworkService for our Zygisk module during system_server specialization.
SystemServerService.registerProxyService(proxyServiceName)
// Start Environmental Daemons
@@ -107,7 +107,7 @@ object VectorDaemon {
// to do so while we still have root. On a successful injection a binder thread opens it for
// us during specialization, but when the injection fails nothing else has, and the daemon
// used to die here on an unreadable preference.
- val isVerboseLog = ManagerService.isVerboseLog()
+ val isVerboseLog = ManagerService.isVerboseLogEnabled()
// Setup IPC channel for applications by injecting DaemonService binder
sendToBridge(VectorService.asBinder(), false, systemServerMaxRetry)
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt
index 53fbef318..23f83e06f 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt
@@ -15,16 +15,16 @@ import android.util.Log
import hidden.HiddenApiBridge
import io.github.libxposed.service.IXposedScopeCallback
import kotlinx.coroutines.launch
-import org.lsposed.lspd.models.Application
+import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.ipc.IVectorDaemon
import org.matrix.vector.ipc.IFrameworkService
import org.matrix.vector.daemon.data.ConfigCache
import org.matrix.vector.daemon.data.ModuleDatabase
import org.matrix.vector.daemon.data.PreferenceStore
import org.matrix.vector.daemon.data.ProcessScope
-import org.matrix.vector.daemon.ipc.ApplicationService
+import org.matrix.vector.daemon.ipc.FrameworkService
import org.matrix.vector.daemon.ipc.ManagerService
-import org.matrix.vector.daemon.ipc.ModuleService
+import org.matrix.vector.daemon.ipc.ModuleAppService
import org.matrix.vector.daemon.system.*
private const val TAG = "VectorService"
@@ -75,7 +75,7 @@ object VectorService : IVectorDaemon.Stub() {
Log.w(TAG, "Unauthorized attachProcess call")
return null
}
- if (ApplicationService.hasRegister(uid, pid)) return null
+ if (FrameworkService.hasRegister(uid, pid)) return null
val scope = ProcessScope(processName, uid)
if (!ManagerService.tryRegisterManagerProcess(pid, uid, processName) &&
@@ -84,8 +84,8 @@ object VectorService : IVectorDaemon.Stub() {
return null
}
- return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) {
- ApplicationService
+ return if (FrameworkService.registerHeartBeat(uid, pid, processName, heartBeat)) {
+ FrameworkService
} else null
}
@@ -196,15 +196,15 @@ object VectorService : IVectorDaemon.Stub() {
// UID Observer
val uidObserver =
object : android.app.IUidObserver.Stub() {
- override fun onUidActive(uid: Int) = ModuleService.uidStarts(uid)
+ override fun onUidActive(uid: Int) = ModuleAppService.uidStarts(uid)
override fun onUidCachedChanged(uid: Int, cached: Boolean) {
- if (!cached) ModuleService.uidStarts(uid)
+ if (!cached) ModuleAppService.uidStarts(uid)
}
- override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleService.uidStarts(uid)
+ override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleAppService.uidStarts(uid)
- override fun onUidGone(uid: Int, disabled: Boolean) = ModuleService.uidGone(uid)
+ override fun onUidGone(uid: Int, disabled: Boolean) = ModuleAppService.uidGone(uid)
}
val which =
@@ -316,7 +316,7 @@ object VectorService : IVectorDaemon.Stub() {
val scopeList = ModuleDatabase.getModuleScope(xposedModule) ?: mutableListOf()
val newScope =
- Application().apply {
+ ScopeEntry().apply {
this.packageName = moduleName
this.userId = userId
}
@@ -367,7 +367,7 @@ object VectorService : IVectorDaemon.Stub() {
if (moduleName != null && isXposedModule && !isRemovedAction && !isRemovedForAllUsers) {
val scopes = ModuleDatabase.getModuleScope(moduleName) ?: emptyList()
val isSystemModule = scopes.any { it.packageName == "system" }
- val isEnabled = ManagerService.enabledModules().contains(moduleName)
+ val isEnabled = ManagerService.getEnabledModules().contains(moduleName)
NotificationManager.notifyModuleUpdated(moduleName, userId, isEnabled, isSystemModule)
}
@@ -455,7 +455,7 @@ object VectorService : IVectorDaemon.Stub() {
val storedUserId = if (scopePackageName == "system") 0 else userId
if (scopes.none { it.packageName == scopePackageName && it.userId == storedUserId }) {
scopes.add(
- Application().apply {
+ ScopeEntry().apply {
this.packageName = scopePackageName
this.userId = storedUserId
})
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt
index ca909dc13..e81bf8310 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt
@@ -12,14 +12,13 @@ import java.nio.file.attribute.PosixFilePermissions
import java.util.UUID
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
-import org.lsposed.lspd.ILSPManagerService
-import org.lsposed.lspd.models.Application
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.ipc.LoadedModule
import org.matrix.vector.daemon.BuildConfig
import org.matrix.vector.daemon.VectorDaemon
-import org.matrix.vector.daemon.ipc.ApplicationService
+import org.matrix.vector.daemon.ipc.FrameworkService
import org.matrix.vector.daemon.ipc.InjectedModuleService
-import org.matrix.vector.daemon.ipc.ModuleService
+import org.matrix.vector.daemon.ipc.ModuleAppService
import org.matrix.vector.daemon.system.*
import org.matrix.vector.daemon.utils.InstallerVerifier
import org.matrix.vector.daemon.utils.applySqliteHelperWorkaround
@@ -264,7 +263,7 @@ object ConfigCache {
// module the user did enable, and they would find the switch off with no reason given.
// The configuration stands; what could not be done is recorded and reported instead.
Log.w(TAG, "Failed to find path of $pkgName")
- unloadable[pkgName] = ILSPManagerService.MODULE_LOAD_NO_APK
+ unloadable[pkgName] = IManagerService.MODULE_LOAD_NO_APK
return@forEach
}
apkPath = realApkPath
@@ -284,7 +283,7 @@ object ConfigCache {
// being first in the list and answering for packages it does not even hold.
// The resolution above now deliberately prefers a *holder*, so for a module only
// user 11 has this reads 1110136, and without the modulo the module would fail
- // its own authentication in `ModuleService.ensureModule` against a caller's
+ // its own authentication in `ModuleAppService.ensureModule` against a caller's
// 10136 and never be sent its binder.
appId = appInfo.uid % PER_USER_RANGE
versionCode = pkgInfo.longVersionCode
@@ -302,11 +301,11 @@ object ConfigCache {
// than reported as "the framework could not load it" alongside a zip that will not parse.
ModuleLoad.UnsupportedApi -> {
Log.w(TAG, "Could not load $pkgName: it targets libxposed API 100; skipping.")
- unloadable[pkgName] = ILSPManagerService.MODULE_LOAD_UNSUPPORTED_API
+ unloadable[pkgName] = IManagerService.MODULE_LOAD_UNSUPPORTED_API
}
ModuleLoad.Unusable -> {
Log.w(TAG, "Could not load $pkgName; skipping.")
- unloadable[pkgName] = ILSPManagerService.MODULE_LOAD_UNUSABLE
+ unloadable[pkgName] = IManagerService.MODULE_LOAD_UNUSABLE
}
}
}
@@ -435,12 +434,12 @@ object ConfigCache {
// Targets are removed only after the module set has been published.
(oldState.modules.keys - newModules.keys).forEach {
- ApplicationService.forgetHotReloadTargets(it)
+ FrameworkService.forgetHotReloadTargets(it)
}
- ApplicationService.backfillLoadedVersions()
+ FrameworkService.backfillLoadedVersions()
// Ask stale opt-in targets to load the generation that was just installed.
- newModules.values.forEach { ModuleService.autoHotReload(it) }
+ newModules.values.forEach { ModuleAppService.autoHotReload(it) }
// Log.d(TAG, "cached modules:")
// newModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") }
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
index 814bd7670..a657223d2 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
@@ -393,7 +393,7 @@ object FileSystem {
fun getPreloadDex(obfuscate: Boolean): SharedMemory? {
if (preloadDex == null) {
runCatching {
- FileInputStream("framework/lspd.dex").use { preloadDex = readDex(it, obfuscate) }
+ FileInputStream("framework/vector.dex").use { preloadDex = readDex(it, obfuscate) }
}
.onFailure { Log.e(TAG, "Failed to load framework dex", it) }
}
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt
index 1e3793427..32a0acc85 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt
@@ -3,7 +3,7 @@ package org.matrix.vector.daemon.data
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.util.Log
-import org.lsposed.lspd.models.Application
+import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.daemon.system.NotificationManager
private const val TAG = "VectorModuleDatabase"
@@ -39,9 +39,9 @@ object ModuleDatabase {
/** The one database handle. [ConfigCache], [PreferenceStore] and the CLI all borrow it. */
val dbHelper = Database()
- fun getModuleScope(packageName: String): MutableList? {
+ fun getModuleScope(packageName: String): MutableList? {
if (packageName == "lspd") return null
- val result = mutableListOf()
+ val result = mutableListOf()
dbHelper.readableDatabase
.query(
"scope INNER JOIN modules ON scope.mid = modules.mid",
@@ -54,7 +54,7 @@ object ModuleDatabase {
.use { cursor ->
while (cursor.moveToNext()) {
result.add(
- Application().apply {
+ ScopeEntry().apply {
this.packageName = cursor.getString(0)
this.userId = cursor.getInt(1)
})
@@ -203,8 +203,11 @@ object ModuleDatabase {
put("apk_path", "") // defer to cache updating
put("enabled", 1)
}
- db.insert("modules", null, values)
- changed = true
+ // `insert` answers -1 rather than throwing: it catches the SQLException itself and logs one
+ // line. Taking that for granted reported a write that never landed as a success, and the
+ // caller acted on it — the manager left its switch on, and the shade's "not activated yet"
+ // notice was cancelled for a module the database had no row for.
+ changed = db.insert("modules", null, values) != -1L
} else {
val values = ContentValues().apply { put("enabled", 1) }
changed = db.update("modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0
@@ -247,7 +250,7 @@ object ModuleDatabase {
return changed
}
- fun setModuleScope(packageName: String, scope: MutableList): Boolean {
+ fun setModuleScope(packageName: String, scope: MutableList): Boolean {
// Last line of defence for staticScope. The manager, the socket CLI, a backup restore and a
// module's own requestScope all end up here, so refusing here covers every one of them.
ConfigCache.staticScopeOf(packageName)?.let { claimed ->
@@ -271,8 +274,8 @@ object ModuleDatabase {
for (app in scope) {
// A module is one package, one APK and one scope set for the whole device — Android cannot
// hold two different builds under one package name, so there is nothing here to key by
- // user. What [Application.userId] names is the *target*: which installed instance of
- // [Application.packageName] this row points at. `ConfigCache` refuses to expand a row whose
+ // user. What [ScopeEntry.userId] names is the *target*: which installed instance of
+ // [ScopeEntry.packageName] this row points at. `ConfigCache` refuses to expand a row whose
// user does not hold the module, which is what keeps a module installed for one user out of
// another user's processes.
//
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt
index d8a16e600..7db27300f 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt
@@ -14,17 +14,17 @@ import java.io.FileInputStream
import java.nio.file.Files
import java.nio.file.Paths
import kotlinx.coroutines.launch
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.daemon.VectorDaemon
private const val TAG = "VectorDex2Oat"
-// Compatibility states mirrored directly from the ILSPManagerService AIDL contract.
-val DEX2OAT_OK = ILSPManagerService.DEX2OAT_OK
-val DEX2OAT_MOUNT_FAILED = ILSPManagerService.DEX2OAT_MOUNT_FAILED
-val DEX2OAT_SEPOLICY_INCORRECT = ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT
-val DEX2OAT_SELINUX_PERMISSIVE = ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE
-val DEX2OAT_CRASHED = ILSPManagerService.DEX2OAT_CRASHED
+// Wrapper states mirrored directly from the IManagerService AIDL contract.
+val DEX2OAT_OK = IManagerService.DEX2OAT_OK
+val DEX2OAT_MOUNT_FAILED = IManagerService.DEX2OAT_MOUNT_FAILED
+val DEX2OAT_SEPOLICY_INCORRECT = IManagerService.DEX2OAT_SEPOLICY_INCORRECT
+val DEX2OAT_SELINUX_PERMISSIVE = IManagerService.DEX2OAT_SELINUX_PERMISSIVE
+val DEX2OAT_CRASHED = IManagerService.DEX2OAT_CRASHED
object Dex2OatServer {
private const val WRAPPER32 = "bin/dex2oat32"
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt
index 85d2a89a1..ffedb0e9f 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt
@@ -4,7 +4,7 @@ import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import io.github.libxposed.service.IXposedService
-import org.lsposed.lspd.models.Application
+import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.daemon.BuildConfig
import org.matrix.vector.daemon.CliRequest
import org.matrix.vector.daemon.CliResponse
@@ -53,7 +53,7 @@ object CliHandler {
// Asked of the configuration, not of the cache. The cache holds what could be *loaded* and
// is rebuilt asynchronously, so the CLI used to report a module the user had just enabled
- // as disabled, and disagree with both the manager and `ManagerService.enabledModules()`.
+ // as disabled, and disagree with both the manager and `ManagerService.getEnabledModules()`.
val enabledModuleKeys = ModuleDatabase.enabledModules().toSet()
// Get all installed modules from the system
val installed = ConfigCache.getInstalledModules()
@@ -134,7 +134,7 @@ object CliHandler {
val user = parts.getOrNull(1)?.toIntOrNull() ?: 0
if (scope.none { it.packageName == pkg && it.userId == user }) {
scope.add(
- Application().apply {
+ ScopeEntry().apply {
packageName = pkg
userId = user
})
@@ -147,13 +147,13 @@ object CliHandler {
if (apps.isEmpty())
throw IllegalArgumentException("No target apps provided for scope overwrite.")
rejectBeyondStaticScope(apps)
- val scope = mutableListOf()
+ val scope = mutableListOf()
apps.forEach { appStr ->
val parts = appStr.split("/")
val pkg = parts[0]
val user = parts.getOrNull(1)?.toIntOrNull() ?: 0
scope.add(
- Application().apply {
+ ScopeEntry().apply {
packageName = pkg
userId = user
})
@@ -184,8 +184,8 @@ object CliHandler {
val key = keys[0]
val value =
when (key) {
- "status-notification" -> ManagerService.enableStatusNotification()
- "verbose-log" -> ManagerService.isVerboseLog
+ "status-notification" -> ManagerService.isStatusNotificationEnabled()
+ "verbose-log" -> ManagerService.isVerboseLogEnabled()
else -> throw IllegalArgumentException("Unknown config key: $key")
}
mapOf("KEY" to key, "VALUE" to value)
@@ -198,8 +198,8 @@ object CliHandler {
?: throw IllegalArgumentException("Value must be 'true' or 'false'.")
when (key) {
- "status-notification" -> ManagerService.setEnableStatusNotification(value)
- "verbose-log" -> ManagerService.setVerboseLog(value)
+ "status-notification" -> ManagerService.setStatusNotificationEnabled(value)
+ "verbose-log" -> ManagerService.setVerboseLogEnabled(value)
else -> throw IllegalArgumentException("Unknown config key: $key")
}
"Successfully set $key to $value."
@@ -271,7 +271,7 @@ object CliHandler {
return when (request.action) {
"clear" -> {
val verbose = request.options["verbose"] as? Boolean ?: false
- ManagerService.clearLogs(verbose)
+ ManagerService.startNewLogPart(verbose)
"Logs cleared successfully."
}
// "stream" is handled in SystemServerService.kt to attach the FileDescriptor
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt
similarity index 95%
rename from daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt
rename to daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt
index 247f66cab..44ed1c650 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt
@@ -20,7 +20,7 @@ import org.matrix.vector.daemon.system.PER_USER_RANGE
import org.matrix.vector.daemon.utils.InstallerVerifier
import org.matrix.vector.daemon.utils.ObfuscationManager
-private const val TAG = "VectorAppService"
+private const val TAG = "VectorFrameworkService"
// Hardcoded transaction code from BridgeService
const val BRIDGE_TRANSACTION_CODE =
@@ -30,7 +30,17 @@ const val DEX_TRANSACTION_CODE =
const val OBFUSCATION_MAP_TRANSACTION_CODE =
('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code
-object ApplicationService : IFrameworkService.Stub() {
+/**
+ * What an injected process asks the framework for — this project's `IFrameworkService`.
+ *
+ * Also the daemon's register of which process is running which module, because answering
+ * `getModules` is what makes a process a hot reload target for each module returned. See
+ * `IFrameworkService.aidl` for who may call what and how a caller is authenticated.
+ *
+ * Was called `ApplicationService`, which named neither the interface it implements nor anything it
+ * does: nothing here is about an `Application`.
+ */
+object FrameworkService : IFrameworkService.Stub() {
data class ProcessKey(val uid: Int, val pid: Int)
@@ -271,7 +281,7 @@ object ApplicationService : IFrameworkService.Stub() {
override fun getLegacyModules() = getAllModules().filter { it.code.legacy }
- override fun isLogMuted(): Boolean = !ManagerService.isVerboseLog
+ override fun isLogMuted(): Boolean = !ManagerService.isVerboseLogEnabled()
override fun getPrefsPath(packageName: String): String {
val info = ensureRegistered()
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt
index b331df99a..e5436c593 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt
@@ -17,6 +17,13 @@ import org.matrix.vector.daemon.system.PER_USER_RANGE
private const val TAG = "VectorInjectedModuleService"
+/**
+ * A module's service as an **injected process** sees it — this project's `IModuleService`.
+ *
+ * The counterpart to [ModuleAppService], and see `IModuleService.aidl` for why the two differ: this
+ * side may only read the module's remote files, because the process holding it runs as the app it
+ * was injected into rather than as the module.
+ */
class InjectedModuleService(private val packageName: String) : IModuleService.Stub() {
// Tracks active RemotePreferenceCallbacks linked by config group. Preferences are stored per
@@ -75,7 +82,7 @@ class InjectedModuleService(private val packageName: String) : IModuleService.St
.getOrElse { throw RemoteException(it.message) }
}
- // Called by ModuleService when the module app has changed the group for one Android user.
+ // Called by ModuleAppService when the module app has changed the group for one Android user.
fun onUpdateRemotePreferences(group: String, userId: Int, diff: Bundle) {
val groupCallbacks = callbacks[group] ?: return
for (subscriber in groupCallbacks) {
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt
index 41b0a3beb..96251dffc 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt
@@ -24,10 +24,12 @@ import hidden.HiddenApiBridge
import io.github.libxposed.service.IXposedService
import java.io.File
import java.util.concurrent.CountDownLatch
-import org.lsposed.lspd.IFrameworkInstallCallback
-import org.lsposed.lspd.ILSPManagerService
-import org.lsposed.lspd.models.Application
-import org.lsposed.lspd.models.UserInfo
+import java.util.concurrent.TimeUnit
+import org.matrix.vector.ipc.DeviceUser
+import org.matrix.vector.ipc.IFrameworkInstallReceiver
+import org.matrix.vector.ipc.IManagerService
+import org.matrix.vector.ipc.ModuleLoadFailure
+import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.daemon.BuildConfig
import org.matrix.vector.daemon.VectorDaemon
import org.matrix.vector.daemon.data.ConfigCache
@@ -46,11 +48,19 @@ import rikka.parcelablelist.ParcelableListSlice
private const val TAG = "VectorManagerService"
-object ManagerService : ILSPManagerService.Stub() {
+object ManagerService : IManagerService.Stub() {
/** AOSP's switch for the synthesised launcher entries Android 10 introduced. */
private const val SHOW_HIDDEN_ICON_APPS = "show_hidden_icon_apps_enabled"
+ /**
+ * How long [uninstallPackage] waits for the package installer to report back.
+ *
+ * Generous rather than tight: the work is real and a loaded device can take a while over it. What
+ * it exists to bound is the case where the status never comes at all.
+ */
+ private const val UNINSTALL_TIMEOUT_SECONDS = 60L
+
private var managerPid = -1
private var pendingManager = false
@@ -217,13 +227,15 @@ object ManagerService : ILSPManagerService.Stub() {
fun isRunningManager(pid: Int, uid: Int): Boolean =
pid == managerPid && ConfigCache.isManager(uid)
- override fun getXposedApiVersion() = IXposedService.LIB_API
+ override fun getProtocolVersion() = IManagerService.PROTOCOL_VERSION
- override fun getXposedVersionCode() = BuildConfig.VERSION_CODE
+ override fun getLibxposedApiVersion() = IXposedService.LIB_API
- override fun getXposedVersionName() = BuildConfig.VERSION_NAME
+ override fun getFrameworkVersionCode() = BuildConfig.VERSION_CODE
- override fun getFrameworkCommit(): String? = BuildConfig.VERSION_HASH.takeIf { it.isNotBlank() }
+ override fun getFrameworkVersionName() = BuildConfig.VERSION_NAME
+
+ override fun getBuildStamp(): String? = BuildConfig.VERSION_HASH.takeIf { it.isNotBlank() }
override fun getInstalledPackagesFromAllUsers(
flags: Int,
@@ -233,18 +245,29 @@ object ManagerService : ILSPManagerService.Stub() {
packageManager?.getInstalledPackagesFromAllUsers(flags, filterNoProcess) ?: emptyList())
}
- override fun enabledModules() = ModuleDatabase.enabledModules()
-
- override fun getUnloadableModules() = ConfigCache.state.unloadable.keys.toTypedArray()
-
- override fun getModuleLoadState(packageName: String) =
- ConfigCache.state.unloadable[packageName] ?: ILSPManagerService.MODULE_LOAD_OK
+ override fun getEnabledModules() = ModuleDatabase.enabledModules().toList()
- override fun enableModule(packageName: String) = ModuleDatabase.enableModule(packageName)
+ /**
+ * The unloadable map, as a list of rows.
+ *
+ * The map is exactly what the pair this replaced sent one key and one lookup at a time, so the
+ * conversion is the whole of the merge. A reason of 0 is never stored — [ConfigCache] only ever
+ * writes one of the three failures — so the AIDL's promise that 0 never travels holds without a
+ * filter here.
+ */
+ override fun getModuleLoadFailures(): List =
+ ConfigCache.state.unloadable.map { (pkgName, why) ->
+ ModuleLoadFailure().apply {
+ packageName = pkgName
+ reason = why
+ }
+ }
- override fun disableModule(packageName: String) = ModuleDatabase.disableModule(packageName)
+ override fun setModuleEnabled(packageName: String, enabled: Boolean) =
+ if (enabled) ModuleDatabase.enableModule(packageName)
+ else ModuleDatabase.disableModule(packageName)
- override fun setModuleScope(packageName: String, scope: MutableList) =
+ override fun setModuleScope(packageName: String, scope: MutableList) =
ModuleDatabase.setModuleScope(packageName, scope)
override fun getModuleScope(packageName: String) = ModuleDatabase.getModuleScope(packageName)
@@ -254,11 +277,11 @@ object ManagerService : ILSPManagerService.Stub() {
// never read false, so its switch snapped back on every tap and had to be greyed out. The OR was
// redundant anyway — `isVerboseLogEnabled()` already defaults to true — so a debug build still
// logs verbosely out of the box, and now a developer can also turn it off.
- override fun isVerboseLog() = PreferenceStore.isVerboseLogEnabled()
+ override fun isVerboseLogEnabled() = PreferenceStore.isVerboseLogEnabled()
- override fun setVerboseLog(enabled: Boolean) {
+ override fun setVerboseLogEnabled(enabled: Boolean) {
PreferenceStore.setVerboseLog(enabled)
- if (isVerboseLog()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose()
+ if (isVerboseLogEnabled()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose()
}
override fun getLogParts(verbose: Boolean): List = FileSystem.listLogParts(verbose)
@@ -268,26 +291,25 @@ object ManagerService : ILSPManagerService.Stub() {
ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY)
}
- override fun getVerboseLog() =
- LogcatMonitor.getVerboseLog()?.let {
- ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY)
- }
-
- override fun getModulesLog(): ParcelFileDescriptor? {
- LogcatMonitor.checkLogFile()
- return LogcatMonitor.getModulesLog()?.let {
- ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY)
- }
+ /**
+ * The part being written on one of the two streams.
+ *
+ * The two calls this replaces were not symmetric: only the modules one asked
+ * [LogcatMonitor.checkLogFile] to re-open a descriptor the reader had lost. That asymmetry is
+ * kept exactly as it was rather than tidied away, because levelling it either way changes when a
+ * lost descriptor is repaired, and that is a decision about the log rather than about this
+ * merge.
+ */
+ override fun getLiveLogPart(verbose: Boolean): ParcelFileDescriptor? {
+ if (!verbose) LogcatMonitor.checkLogFile()
+ val file = if (verbose) LogcatMonitor.getVerboseLog() else LogcatMonitor.getModulesLog()
+ return file?.let { ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY) }
}
- override fun clearLogs(verbose: Boolean): Boolean {
+ override fun startNewLogPart(verbose: Boolean) {
LogcatMonitor.refresh(verbose)
- return true
}
- override fun getPackageInfo(packageName: String, flags: Int, uid: Int) =
- packageManager?.getPackageInfoCompat(packageName, flags, uid)
-
override fun forceStopPackage(packageName: String, userId: Int) {
activityManager?.forceStopPackage(packageName, userId)
}
@@ -297,7 +319,7 @@ object ManagerService : ILSPManagerService.Stub() {
/**
* The flashed manager APK, verified, for the manager to install as an ordinary app.
*
- * The same file and the same check as [ApplicationService.openManagerApk], which
+ * The same file and the same check as [FrameworkService.openManagerApk], which
* serves it to the host process for injection — one APK, one signature gate, whichever way it
* leaves the module directory.
*/
@@ -359,18 +381,31 @@ object ManagerService : ILSPManagerService.Stub() {
.getOrNull() ?: return false
val pkg = VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST)
- val flag = if (userId == -1) 0x00000002 else 0 // DELETE_ALL_USERS flag
+ val allUsers = userId == IManagerService.ALL_USERS
+ val flag = if (allUsers) 0x00000002 else 0 // DELETE_ALL_USERS flag
runCatching {
packageManager
?.packageInstaller
- ?.uninstall(pkg, "android", flag, intentSender, if (userId == -1) 0 else userId)
+ ?.uninstall(pkg, "android", flag, intentSender, if (allUsers) 0 else userId)
}
.onFailure {
return false
}
- latch.await()
+ // Bounded, because this runs on a binder thread and the status is a broadcast the package
+ // installer may never send — a device-policy refusal, a user removed mid-uninstall, a wedged
+ // system service. An unbounded wait held that thread for the life of the daemon, and enough of
+ // them exhaust the pool, at which point every call from the manager and from every injected
+ // process queues behind an uninstall nobody is still watching.
+ //
+ // A timeout is not a failure of the uninstall, only of our knowledge of it, so it answers false
+ // for the same reason a refusal does: the caller must not be told a package is gone on the
+ // strength of a status that never arrived.
+ if (!latch.await(UNINSTALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ Log.w(TAG, "No uninstall status for $packageName after ${UNINSTALL_TIMEOUT_SECONDS}s")
+ return false
+ }
return result
}
@@ -378,27 +413,19 @@ object ManagerService : ILSPManagerService.Stub() {
SELinux.checkSELinuxAccess(
"u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans")
- override fun getUsers(): List {
+ override fun getUsers(): List {
return userManager?.getRealUsers()?.map {
- UserInfo().apply {
+ DeviceUser().apply {
id = it.id
name = it.name
}
} ?: emptyList()
}
- override fun installExistingPackageAsUser(packageName: String, userId: Int): Int {
- return runCatching {
- packageManager?.installExistingPackageAsUser(packageName, userId, 0, 0, null) ?: -110
- }
- .getOrDefault(-110)
- }
-
- override fun systemServerRequested() = SystemServerService.systemServerRequested
+ override fun isSystemServerAttached() = SystemServerService.systemServerRequested
- override fun startActivityAsUserWithFeature(intent: Intent, userId: Int): Int {
- if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) {
- intent.removeExtra("lsp_no_switch_to_user")
+ override fun startActivityAsUser(intent: Intent, userId: Int, noUserSwitch: Boolean): Int {
+ if (!noUserSwitch) {
val currentUser = activityManager?.currentUser
val parent = userManager?.getProfileParent(userId)?.id ?: userId
if (currentUser != null && currentUser.id != parent) {
@@ -422,7 +449,7 @@ object ManagerService : ILSPManagerService.Stub() {
?: emptyList())
}
- override fun dex2oatFlagsLoaded() =
+ override fun isDex2OatInliningDisabled() =
SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0")
/**
@@ -449,7 +476,7 @@ object ManagerService : ILSPManagerService.Stub() {
.onFailure { Log.w(TAG, "setForcedLauncherIcons failed", it) }
}
- override fun forcedLauncherIcons(): Boolean =
+ override fun isForcedLauncherIcons(): Boolean =
runCatching {
// Unset must read as the platform default of 1, not as "off" — otherwise the switch
// shows the opposite of what the system is doing on every device where nobody has
@@ -472,16 +499,15 @@ object ManagerService : ILSPManagerService.Stub() {
return output.ifBlank { null }
}
- override fun getLogs(zipFd: ParcelFileDescriptor) {
+ override fun writeBugReport(zipFd: ParcelFileDescriptor) {
FileSystem.getLogs(zipFd)
}
- override fun restartFor(intent: Intent) {} // No-op matching original
- override fun enableStatusNotification() = PreferenceStore.isStatusNotificationEnabled()
+ override fun isStatusNotificationEnabled() = PreferenceStore.isStatusNotificationEnabled()
- override fun setEnableStatusNotification(enable: Boolean) {
- val isEnabled = enableStatusNotification()
+ override fun setStatusNotificationEnabled(enable: Boolean) {
+ val isEnabled = isStatusNotificationEnabled()
PreferenceStore.setStatusNotification(enable)
if (isEnabled && !enable) {
NotificationManager.cancelStatusNotification()
@@ -493,7 +519,7 @@ object ManagerService : ILSPManagerService.Stub() {
override fun optimizePackage(packageName: String) = PackageOptimizer.optimize(packageName)
- override fun getDex2OatWrapperCompatibility() =
+ override fun getDex2OatWrapperState() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dex2OatServer.compatibility else 0
override fun setIncludeNewApps(packageName: String, enabled: Boolean) =
@@ -503,24 +529,23 @@ object ManagerService : ILSPManagerService.Stub() {
override fun getRootImplementation() = RootImplementation.implementation
- override fun getRootImplementationVersion() = RootImplementation.version
- override fun installFrameworkZip(zipPath: String, callback: IFrameworkInstallCallback) {
+ override fun installFrameworkZip(zipPath: String, receiver: IFrameworkInstallReceiver) {
// Off the binder thread: a flash takes seconds to minutes, and holding a binder thread for its
// duration starves everything else the manager asks of the daemon meanwhile — including the
// log reads the install screen is doing to show what is happening.
Thread {
val exit =
RootImplementation.install(zipPath) { line ->
- runCatching { callback.onLine(line) }
+ runCatching { receiver.onLine(line) }
.onFailure {
// The manager went away mid-flash. Keep installing — stopping now would
// leave the module tree half-written — and keep logging, which is the only
// record left.
- Log.w(TAG, "Install callback is gone; continuing", it)
+ Log.w(TAG, "Install receiver is gone; continuing", it)
}
}
- runCatching { callback.onFinished(exit) }
+ runCatching { receiver.onFinished(exit) }
.onFailure { Log.w(TAG, "Could not report install result", it) }
}
.apply {
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleAppService.kt
similarity index 91%
rename from daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt
rename to daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleAppService.kt
index 31c0a56a9..8a3bbcd52 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleAppService.kt
@@ -31,9 +31,20 @@ import org.matrix.vector.daemon.system.ProcessFreezer
import org.matrix.vector.daemon.system.PER_USER_RANGE
import org.matrix.vector.daemon.system.activityManager
-private const val TAG = "VectorModuleService"
-
-class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stub() {
+private const val TAG = "VectorModuleAppService"
+
+/**
+ * A module's service as its own **app** sees it — libxposed's `IXposedService`.
+ *
+ * One of two services a module gets, and the name says which. [InjectedModuleService] is the other:
+ * the same module seen from inside a process it was injected into. They deliberately differ in what
+ * they allow — a module app may write its remote files, a hooked process may only read them,
+ * because a hooked process runs as the app it was injected into rather than as the module — so
+ * which one a reader is looking at has to be legible from the class name.
+ *
+ * See `IModuleService.aidl` for the other side of that distinction.
+ */
+class ModuleAppService(private val loadedModule: LoadedModule) : IXposedService.Stub() {
companion object {
// Per-target serialization lives on the target itself; this only keeps one slow target from
@@ -47,7 +58,8 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
private const val RELOAD_TIMEOUT_SECONDS = 30L
private val uidSet = ConcurrentHashMap.newKeySet()
- private val serviceMap = Collections.synchronizedMap(WeakHashMap())
+ private val serviceMap =
+ Collections.synchronizedMap(WeakHashMap())
fun uidClear() {
uidSet.clear()
@@ -57,7 +69,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
if (uidSet.add(uid)) {
val module = ConfigCache.getModuleByUid(uid)
if (module?.code?.legacy == false) {
- val service = serviceMap.getOrPut(module) { ModuleService(module) }
+ val service = serviceMap.getOrPut(module) { ModuleAppService(module) }
service.sendBinder(uid)
}
}
@@ -70,9 +82,9 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
// Drives the same cycle as a service request, so onHotReloading can still refuse it.
fun autoHotReload(module: LoadedModule) {
if (!module.code.autoHotReload) return
- val service = serviceMap.getOrPut(module) { ModuleService(module) }
- ApplicationService.staleHotReloadTargets(module.packageName).forEach { target ->
- if (target.hotReloadable && ApplicationService.beginHotReload(target)) {
+ val service = serviceMap.getOrPut(module) { ModuleAppService(module) }
+ FrameworkService.staleHotReloadTargets(module.packageName).forEach { target ->
+ if (target.hotReloadable && FrameworkService.beginHotReload(target)) {
Log.d(TAG, "Auto hot reloading ${module.packageName} in ${target.processName}")
hotReloadExecutor.execute { service.runHotReload(target, null, null) }
}
@@ -219,7 +231,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
override fun getRunningTargets(): List {
val userId = ensureModule()
- return ApplicationService.getHotReloadTargets(loadedModule.packageName, userId)
+ return FrameworkService.getHotReloadTargets(loadedModule.packageName, userId)
}
override fun hotReloadModule(targetId: Long, data: Bundle?, callback: IHotReloadCallback?) {
@@ -233,7 +245,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
// raised for anything else on this path - a module-thrown SecurityException in particular has
// to reach the caller as a FAILED result, not as "invalid target id".
val target =
- ApplicationService.getHotReloadTarget(targetId, loadedModule.packageName, userId)
+ FrameworkService.getHotReloadTarget(targetId, loadedModule.packageName, userId)
?: throw SecurityException("Target $targetId is not a target of ${loadedModule.packageName}")
if (!target.hotReloadable) {
@@ -242,7 +254,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
return
}
- if (!ApplicationService.beginHotReload(target)) {
+ if (!FrameworkService.beginHotReload(target)) {
report(callback, IXposedService.HOT_RELOAD_IN_PROGRESS, "A reload is already running")
return
}
@@ -254,7 +266,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
}
private fun runHotReload(
- target: ApplicationService.HotReloadTarget,
+ target: FrameworkService.HotReloadTarget,
data: Bundle?,
callback: IHotReloadCallback?,
) {
@@ -266,7 +278,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
var outcome: HotReloadOutcome? = null
try {
- val binder = ApplicationService.getHotReloadBinder(target)
+ val binder = FrameworkService.getHotReloadBinder(target)
if (binder == null) {
status = IXposedService.HOT_RELOAD_UNSUPPORTED
message = "Process ${target.processName} has no hot reload entry point"
@@ -306,7 +318,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
// RELOADING for the life of the process, and every later request would answer IN_PROGRESS.
if (!answered.await(RELOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
status =
- if (ApplicationService.isProcessRegistered(target)) IXposedService.HOT_RELOAD_FAILED
+ if (FrameworkService.isProcessRegistered(target)) IXposedService.HOT_RELOAD_FAILED
else IXposedService.HOT_RELOAD_PROCESS_DIED
message =
if (status == IXposedService.HOT_RELOAD_PROCESS_DIED) {
@@ -343,7 +355,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
// Deliberately not keyed on DeadObjectException: a frozen-but-alive target answers a
// transaction with exactly that, so the exception type says nothing about whether the process
// is gone. The heartbeat registry does - it is driven by a DeathRecipient.
- val gone = !ApplicationService.isProcessRegistered(target)
+ val gone = !FrameworkService.isProcessRegistered(target)
status =
if (gone) IXposedService.HOT_RELOAD_PROCESS_DIED else IXposedService.HOT_RELOAD_FAILED
message =
@@ -352,7 +364,7 @@ class ModuleService(private val loadedModule: LoadedModule) : IXposedService.Stu
Log.e(TAG, "Hot reload of ${loadedModule.packageName} failed", t)
} finally {
refreeze?.invoke()
- ApplicationService.endHotReload(target, stateFor(status), loadedVersion)
+ FrameworkService.endHotReload(target, stateFor(status), loadedVersion)
report(callback, status, message)
}
}
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt
index e961f4743..c0db44073 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt
@@ -74,12 +74,16 @@ object SystemServerService : Binder(), IBinder.DeathRecipient {
processLifeToken: IBinder?
): IFrameworkService? {
if (uid != 1000 || processLifeToken == null || processName != "system") return null
- systemServerRequested = true
- // Return the ApplicationService singleton if successfully registered
- return if (ApplicationService.registerHeartBeat(uid, pid, processName, processLifeToken)) {
- ApplicationService
- } else null
+ // Latched only once the registration has actually succeeded, not on the way in. It used to be
+ // set immediately after the gate above, so a registration that then failed — registerHeartBeat
+ // answers false when the life token cannot be linked to death — still read as attached. The
+ // symptom was the worst kind: the manager's status page reported the framework as present in
+ // system_server while no module hooking the system ever loaded, which sends a reader looking
+ // at their module instead of at the injection.
+ if (!FrameworkService.registerHeartBeat(uid, pid, processName, processLifeToken)) return null
+ systemServerRequested = true
+ return FrameworkService
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
@@ -107,7 +111,7 @@ object SystemServerService : Binder(), IBinder.DeathRecipient {
}
DEX_TRANSACTION_CODE,
OBFUSCATION_MAP_TRANSACTION_CODE -> {
- return ApplicationService.onTransact(code, data, reply, flags)
+ return FrameworkService.onTransact(code, data, reply, flags)
}
else -> {
return super.onTransact(code, data, reply, flags)
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt
index 050089e0a..d985b4355 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt
@@ -4,39 +4,26 @@ import android.util.Log
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IFrameworkInstallReceiver
+import org.matrix.vector.ipc.IManagerService
private const val TAG = "VectorRootInstaller"
/**
* Which root implementation is managing this device, and how to flash through it.
*
- * The detection mirrors NeoZygisk's `root_impl` module, deliberately: that is the code deciding
- * whether Vector loads at all on this device, and a manager that disagreed with it about which root
- * is in charge would be reporting on a different device than the one it is running on. Where a
- * version floor can be read at all it is NeoZygisk's own, so "too old to flash through" means the
- * same thing in both places.
+ * Detection is by binary: the binary has to exist to do the flashing anyway, and asking it is one
+ * process spawn for a question asked once. The version it reports is quoted back to the user and
+ * nothing more — whether the zygisk loader will run on this device is the loader's own decision,
+ * taken before the daemon exists, so a daemon that is running has already passed it.
*
- * Detection is by binary rather than by NeoZygisk's ioctl/prctl route, which needs a JNI hop for a
- * question asked once — and the binary has to exist anyway to do the flashing. The cost of that
- * choice is visible in [detectKernelSu], which cannot read a version at all.
- *
- * A binary that exists but *fails* is not an implementation: this device carries a leftover
+ * A binary that exists but *fails* is not an implementation: a device can carry a leftover
* `/data/adb/magisk/magisk` from a previous root manager, and it exits 1 with "Cannot connect to
* daemon". Requiring a clean exit is what stops that from being reported as a second root
* implementation and turning a working KernelSU device into ROOT_MULTIPLE.
*/
object RootImplementation {
- /**
- * NeoZygisk's floors, from its root build.gradle.kts. Below these it will not load.
- *
- * There is deliberately no KernelSU floor here: its version code is not reachable from a shell,
- * and [detectKernelSu] explains why not checking it is correct rather than merely convenient.
- */
- private const val MIN_MAGISK = 26402
- private const val MIN_APATCH = 10762
-
/**
* Where each implementation keeps its binary.
*
@@ -64,9 +51,6 @@ object RootImplementation {
val implementation: Int
get() = detected.implementation
- val version: String?
- get() = detected.version
-
private fun detect(): Detection {
val magisk = detectMagisk()
val ksu = detectKernelSu()
@@ -77,10 +61,10 @@ object RootImplementation {
// Not a failure to detect — a device with two root implementations installed, where
// flashing through either is a coin toss about which one owns the module tree.
Log.w(TAG, "Multiple root implementations: ${found.joinToString { it.version ?: "?" }}")
- return Detection(ILSPManagerService.ROOT_MULTIPLE, found.joinToString { it.version ?: "?" })
+ return Detection(IManagerService.ROOT_MULTIPLE, found.joinToString { it.version ?: "?" })
}
- val only = found.firstOrNull() ?: return Detection(ILSPManagerService.ROOT_NONE, null)
+ val only = found.firstOrNull() ?: return Detection(IManagerService.ROOT_NONE, null)
Log.i(TAG, "Root implementation: ${only.version} via ${only.binary}")
return only
}
@@ -90,51 +74,29 @@ object RootImplementation {
val (binary, raw) = run(MAGISK_PATHS, "-V") ?: return null
val code = raw.trim().toIntOrNull() ?: return null
val name = run(MAGISK_PATHS, "-v")?.second?.trim()?.lineSequence()?.firstOrNull()
- val supported = code >= MIN_MAGISK
- return Detection(
- if (supported) ILSPManagerService.ROOT_MAGISK else ILSPManagerService.ROOT_TOO_OLD,
- "Magisk ${name ?: code}",
- binary,
- )
+ return Detection(IManagerService.ROOT_MAGISK, "Magisk ${name ?: code}", binary)
}
/**
- * KernelSU, which cannot be version-checked from a shell.
- *
- * `ksud -V` prints a *build hash*, not a version code — measured on a KernelSU device it answers
- * `ksud 64e3761d`. An earlier version of this took the first run of digits out of that, read
- * `64`, compared it against the 10940 floor and declared the device too old to flash on — which
- * would have disabled the entire feature on exactly the devices it works on. The version code
- * lives behind KernelSU's prctl/ioctl interface, which is why NeoZygisk reaches for it and why
- * this cannot.
- *
- * So presence is the whole test, and that is sound rather than a shrug: NeoZygisk refuses to load
- * on a KernelSU older than its floor, so a daemon that is running at all is running under one new
- * enough. The check this cannot perform has already been performed, one layer down.
+ * KernelSU. `ksud -V` prints a *build hash* rather than a version code — on a real device it
+ * answers `ksud 64e3761d` — so what is quoted back to the user is that hash.
*/
private fun detectKernelSu(): Detection? {
val (binary, raw) = run(KSUD_PATHS, "-V") ?: return null
val build = raw.trim().substringAfter("ksud ").trim()
- return Detection(ILSPManagerService.ROOT_KERNELSU, "KernelSU ($build)", binary)
+ return Detection(IManagerService.ROOT_KERNELSU, "KernelSU ($build)", binary)
}
/**
- * APatch. `apd -V` prints "apd ", so the second field is the version — NeoZygisk's parse.
- *
- * When that field is not a number, this reports the implementation as present and usable rather
- * than absent. Refusing to flash because *our parser* did not recognise a version string would be
- * refusing on the evidence of our own code rather than on the state of the device — which is the
- * mistake the KernelSU branch above was making.
+ * APatch. `apd -V` prints "apd ", so the second field is the version; when it is not a
+ * number the whole line is quoted instead, because a parser that did not recognise a version
+ * string says nothing about the device.
*/
private fun detectApatch(): Detection? {
val (binary, raw) = run(APD_PATHS, "-V") ?: return null
val output = raw.trim()
val code = output.split(Regex("\\s+")).getOrNull(1)?.toIntOrNull()
- return when {
- code == null -> Detection(ILSPManagerService.ROOT_APATCH, "APatch ($output)", binary)
- code >= MIN_APATCH -> Detection(ILSPManagerService.ROOT_APATCH, "APatch $code", binary)
- else -> Detection(ILSPManagerService.ROOT_TOO_OLD, "APatch $code", binary)
- }
+ return Detection(IManagerService.ROOT_APATCH, "APatch ${code ?: "($output)"}", binary)
}
/**
@@ -167,9 +129,9 @@ object RootImplementation {
private fun installCommand(zipPath: String): List? {
val binary = detected.binary ?: return null
return when (implementation) {
- ILSPManagerService.ROOT_MAGISK -> listOf(binary, "--install-module", zipPath)
- ILSPManagerService.ROOT_KERNELSU -> listOf(binary, "module", "install", zipPath)
- ILSPManagerService.ROOT_APATCH -> listOf(binary, "module", "install", zipPath)
+ IManagerService.ROOT_MAGISK -> listOf(binary, "--install-module", zipPath)
+ IManagerService.ROOT_KERNELSU -> listOf(binary, "module", "install", zipPath)
+ IManagerService.ROOT_APATCH -> listOf(binary, "module", "install", zipPath)
else -> null
}
}
@@ -189,7 +151,7 @@ object RootImplementation {
val message = "Refusing to flash $zipPath: not a readable file"
Log.e(TAG, message)
onLine(message)
- return ILSPManagerService.INSTALL_NO_SUCH_FILE
+ return IFrameworkInstallReceiver.INSTALL_NO_SUCH_FILE
}
val command =
@@ -198,7 +160,7 @@ object RootImplementation {
val message = "No usable root implementation to flash through (code $implementation)"
Log.e(TAG, message)
onLine(message)
- return ILSPManagerService.INSTALL_NO_ROOT
+ return IFrameworkInstallReceiver.INSTALL_NO_ROOT
}
Log.i(TAG, "Flashing ${zip.name} with: ${command.joinToString(" ")}")
@@ -222,7 +184,7 @@ object RootImplementation {
.getOrElse {
Log.e(TAG, "Installer could not be started", it)
onLine("Could not start the installer: ${it.message}")
- ILSPManagerService.INSTALL_NOT_EXECUTED
+ IFrameworkInstallReceiver.INSTALL_NOT_EXECUTED
}
}
}
diff --git a/hiddenapi/bridge/src/main/java/hidden/HiddenApiBridge.java b/hiddenapi/bridge/src/main/java/hidden/HiddenApiBridge.java
index c2aa4d493..744f0c99a 100644
--- a/hiddenapi/bridge/src/main/java/hidden/HiddenApiBridge.java
+++ b/hiddenapi/bridge/src/main/java/hidden/HiddenApiBridge.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package hidden;
import android.app.ActivityManager;
diff --git a/hiddenapi/stubs/src/main/java/android/app/IActivityManager.java b/hiddenapi/stubs/src/main/java/android/app/IActivityManager.java
index 7cbe5220b..e64857b7a 100644
--- a/hiddenapi/stubs/src/main/java/android/app/IActivityManager.java
+++ b/hiddenapi/stubs/src/main/java/android/app/IActivityManager.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package android.app;
import android.content.IIntentReceiver;
diff --git a/hiddenapi/stubs/src/main/java/android/app/IApplicationThread.java b/hiddenapi/stubs/src/main/java/android/app/IApplicationThread.java
index 0b0146fab..eddfe4fb8 100644
--- a/hiddenapi/stubs/src/main/java/android/app/IApplicationThread.java
+++ b/hiddenapi/stubs/src/main/java/android/app/IApplicationThread.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package android.app;
import android.os.Binder;
diff --git a/hiddenapi/stubs/src/main/java/android/app/ProfilerInfo.java b/hiddenapi/stubs/src/main/java/android/app/ProfilerInfo.java
index 6b5056f07..88ed94d90 100644
--- a/hiddenapi/stubs/src/main/java/android/app/ProfilerInfo.java
+++ b/hiddenapi/stubs/src/main/java/android/app/ProfilerInfo.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package android.app;
public class ProfilerInfo {
diff --git a/hiddenapi/stubs/src/main/java/android/content/IIntentReceiver.java b/hiddenapi/stubs/src/main/java/android/content/IIntentReceiver.java
index 233fce190..d4311032f 100644
--- a/hiddenapi/stubs/src/main/java/android/content/IIntentReceiver.java
+++ b/hiddenapi/stubs/src/main/java/android/content/IIntentReceiver.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package android.content;
import android.os.Binder;
diff --git a/hiddenapi/stubs/src/main/java/android/content/Intent.java b/hiddenapi/stubs/src/main/java/android/content/Intent.java
index 48c55e8f0..459e57566 100644
--- a/hiddenapi/stubs/src/main/java/android/content/Intent.java
+++ b/hiddenapi/stubs/src/main/java/android/content/Intent.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package android.content;
public class Intent {
diff --git a/hiddenapi/stubs/src/main/java/android/os/Bundle.java b/hiddenapi/stubs/src/main/java/android/os/Bundle.java
index 24323c6aa..49ff9ee20 100644
--- a/hiddenapi/stubs/src/main/java/android/os/Bundle.java
+++ b/hiddenapi/stubs/src/main/java/android/os/Bundle.java
@@ -1,22 +1,3 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
package android.os;
public class Bundle {
diff --git a/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java b/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java
index e3976013c..654fc6030 100644
--- a/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java
+++ b/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java
@@ -6,7 +6,7 @@
import android.os.Environment;
import android.preference.PreferenceManager;
-import org.lsposed.lspd.util.Utils.Log;
+import org.matrix.vector.util.Log;
import org.matrix.vector.impl.core.VectorServiceClient;
import org.matrix.vector.impl.utils.VectorMetaDataReader;
import org.matrix.vector.legacy.BuildConfig;
diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java
index 0e152909f..7b53cdac1 100644
--- a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java
+++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java
@@ -5,7 +5,7 @@
import android.content.res.TypedArray;
import android.util.Log;
-import org.lsposed.lspd.util.Utils;
+import org.matrix.vector.util.Utils;
import org.matrix.vector.impl.hooks.VectorNativeHooker;
import org.matrix.vector.impl.hooks.VectorLegacyCallback;
import org.matrix.vector.nativebridge.HookBridge;
@@ -138,10 +138,11 @@ public synchronized static void log(String text) {
* @param t The Throwable object for the stack trace.
*/
public synchronized static void log(Throwable t) {
- // Utils.Log's, not android.util.Log's: the latter returns an empty string for any
+ // Written out in full because this file also imports android.util.Log, and it is the
+ // framework's own that is wanted: the platform's returns an empty string for any
// UnknownHostException cause chain, so a module logging a failed request landed an empty
// line in the modules log.
- String logStr = Utils.Log.getStackTraceString(t);
+ String logStr = org.matrix.vector.util.Log.getStackTraceString(t);
Log.e(TAG, logStr);
}
diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedInit.java b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java
index e45c8b3a8..eddc5e477 100644
--- a/legacy/src/main/java/de/robv/android/xposed/XposedInit.java
+++ b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java
@@ -25,7 +25,7 @@
import org.matrix.vector.nativebridge.NativeAPI;
import org.matrix.vector.nativebridge.ResourcesHook;
import org.matrix.vector.ipc.ModuleCode;
-import org.lsposed.lspd.util.Utils.Log;
+import org.matrix.vector.util.Log;
import java.io.File;
import java.lang.ref.WeakReference;
diff --git a/legacy/src/main/java/org/matrix/vector/Startup.java b/legacy/src/main/java/org/matrix/vector/Startup.java
index 596af1ce8..5ca637175 100644
--- a/legacy/src/main/java/org/matrix/vector/Startup.java
+++ b/legacy/src/main/java/org/matrix/vector/Startup.java
@@ -1,7 +1,7 @@
package org.matrix.vector;
import org.matrix.vector.ipc.IFrameworkService;
-import org.lsposed.lspd.util.Utils;
+import org.matrix.vector.util.Utils;
import org.matrix.vector.impl.core.VectorStartup;
import org.matrix.vector.impl.di.VectorBootstrap;
import org.matrix.vector.legacy.LegacyDelegateImpl;
diff --git a/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java b/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java
index 885d40878..8500d52dc 100644
--- a/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java
+++ b/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java
@@ -2,7 +2,7 @@
import android.content.res.XResources;
-import org.lsposed.lspd.util.Utils;
+import org.matrix.vector.util.Utils;
import org.matrix.vector.impl.core.VectorServiceClient;
import org.matrix.vector.impl.di.LegacyFrameworkDelegate;
import org.matrix.vector.impl.di.LegacyPackageInfo;
diff --git a/manager/README.md b/manager/README.md
index 3849de2e2..be6613108 100644
--- a/manager/README.md
+++ b/manager/README.md
@@ -41,13 +41,14 @@ with whatever they managed to fetch before there was a daemon.
`ipc/DaemonClient` wraps every AIDL call in `runIpc`, which moves it to `Dispatchers.IO` and returns
a `Result`. The interface is
-`services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl`.
-
-Two properties of Binder shape most of the mistakes made here. A proxy returns a *default* for a
-transaction the daemon does not implement rather than throwing, so `0`, `null` and empty are
-indistinguishable from real answers — which is why `ROOT_UNKNOWN` is `0` and why an older daemon
-must never be able to answer a question by accident. And a call that succeeded is not a call that
-did anything: several of these return a `boolean` the daemon uses to refuse, and dropping it turns a
+`services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl`, and it is the
+source of truth for what each call means: read the method's documentation there before calling it.
+
+Two properties of Binder shape most of the mistakes made here, and the AIDL spells out what each
+method does about them. A proxy returns a *default* for a transaction the daemon does not implement
+rather than throwing, so `0`, `null` and empty are indistinguishable from real answers — see
+`getProtocolVersion` and `ROOT_UNKNOWN` there. And a call that succeeded is not a call that did
+anything: several of these return a `boolean` the daemon uses to refuse, and dropping it turns a
refusal into a silent success.
The daemon owns the truth. When a write and a read disagree, the read is usually coming from the
diff --git a/manager/proguard-rules.pro b/manager/proguard-rules.pro
index 978023685..469d8bc2c 100644
--- a/manager/proguard-rules.pro
+++ b/manager/proguard-rules.pro
@@ -11,7 +11,7 @@
-keep class org.matrix.vector.manager.ui.MainActivity { (); }
# AIDL stubs and the parcelables crossing the daemon boundary.
--keep class org.lsposed.lspd.** { *; }
+-keep class org.matrix.vector.ipc.** { *; }
-keep class rikka.parcelablelist.** { *; }
# kotlinx.serialization keeps generated serializers reachable from the companion.
diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt
index 211671e9e..ccba13ef4 100644
--- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt
+++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt
@@ -1,6 +1,5 @@
package org.matrix.vector.manager.demo
-import org.lsposed.lspd.ILSPManagerService
import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope
import android.os.Bundle
@@ -29,6 +28,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.manager.di.ServiceLocator
import org.matrix.vector.manager.ui.VectorApp
import org.matrix.vector.manager.ui.theme.LocalizedContent
@@ -72,7 +72,7 @@ class DemoActivity : ComponentActivity() {
* re-asserts the choice whenever something else replaces it. It settles immediately: the next
* emission is the pinned value, which the collector then ignores.
*/
- private var pinned: ILSPManagerService? = null
+ private var pinned: IManagerService? = null
private var pinning = false
diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt
index 7ee890d63..b407e9e26 100644
--- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt
+++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt
@@ -1,6 +1,6 @@
package org.matrix.vector.manager.demo
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IManagerService
/**
* A device state the manager cannot otherwise be shown.
@@ -25,9 +25,9 @@ data class DemoScenario(
/** Delay on every status call. Non-zero is the only way to hold "Checking…" still. */
val stallMillis: Long = 0,
val sepolicyLoaded: Boolean = true,
- val systemServerRequested: Boolean = true,
- val dex2oatFlagsLoaded: Boolean = true,
- val dex2oatCompatibility: Int = ILSPManagerService.DEX2OAT_OK,
+ val systemServerAttached: Boolean = true,
+ val dex2OatInliningDisabled: Boolean = true,
+ val dex2OatWrapperState: Int = IManagerService.DEX2OAT_OK,
/**
* What the framework claims to implement.
@@ -35,10 +35,9 @@ data class DemoScenario(
* Lowering it is how a module becomes incompatible without fabricating a module: the real ones
* on the device declare a real minimum, and the framework simply stops meeting it.
*/
- val xposedApiVersion: Int = -1,
- val xposedVersionCode: Long = -1,
- val rootImplementation: Int = ILSPManagerService.ROOT_MAGISK,
- val rootVersion: String? = "28.1",
+ val libxposedApiVersion: Int = -1,
+ val frameworkVersionCode: Long = -1,
+ val rootImplementation: Int = IManagerService.ROOT_MAGISK,
val install: InstallScript = InstallScript.SUCCEEDS,
/**
@@ -115,23 +114,23 @@ val DEMO_SCENARIOS: List =
id = "system-server",
title = "System framework injection failed",
summary = "Degraded, one cause. Normally needs another root module interfering.",
- systemServerRequested = false,
+ systemServerAttached = false,
),
DemoScenario(
id = "dex2oat",
title = "Dex optimizer wrapper unavailable",
summary = "Degraded, one cause. Needs system properties removed or changed.",
- dex2oatFlagsLoaded = false,
- dex2oatCompatibility = ILSPManagerService.DEX2OAT_MOUNT_FAILED,
+ dex2OatInliningDisabled = false,
+ dex2OatWrapperState = IManagerService.DEX2OAT_MOUNT_FAILED,
),
DemoScenario(
id = "all-issues",
title = "All three causes at once",
summary = "Whether the issue list reads as a list or as a wall.",
sepolicyLoaded = false,
- systemServerRequested = false,
- dex2oatFlagsLoaded = false,
- dex2oatCompatibility = ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT,
+ systemServerAttached = false,
+ dex2OatInliningDisabled = false,
+ dex2OatWrapperState = IManagerService.DEX2OAT_SEPOLICY_INCORRECT,
),
DemoScenario(
id = "inactive",
@@ -149,57 +148,45 @@ val DEMO_SCENARIOS: List =
id = "api-too-old",
title = "Framework below what modules need",
summary = "API 82. Installed modules that need more become incompatible.",
- xposedApiVersion = 82,
+ libxposedApiVersion = 82,
),
DemoScenario(
id = "root-none",
title = "No root implementation",
summary = "Nothing to flash through. The install path must refuse, not fail.",
- rootImplementation = ILSPManagerService.ROOT_NONE,
- rootVersion = null,
+ rootImplementation = IManagerService.ROOT_NONE,
install = DemoScenario.InstallScript.NO_ROOT,
),
DemoScenario(
id = "root-multiple",
title = "Two root implementations fighting",
summary = "Flashing through either would be a guess, and must be named as such.",
- rootImplementation = ILSPManagerService.ROOT_MULTIPLE,
- rootVersion = null,
- install = DemoScenario.InstallScript.NO_ROOT,
- ),
- DemoScenario(
- id = "root-too-old",
- title = "Root implementation too old",
- summary = "Installed but not usable. Distinct from having none.",
- rootImplementation = ILSPManagerService.ROOT_TOO_OLD,
- rootVersion = "20.4",
+ rootImplementation = IManagerService.ROOT_MULTIPLE,
install = DemoScenario.InstallScript.NO_ROOT,
),
DemoScenario(
id = "root-ksu",
title = "KernelSU",
summary = "The install path quotes the implementation it found.",
- rootImplementation = ILSPManagerService.ROOT_KERNELSU,
- rootVersion = "12045",
+ rootImplementation = IManagerService.ROOT_KERNELSU,
),
DemoScenario(
id = "root-apatch",
title = "APatch",
summary = "As above, third implementation.",
- rootImplementation = ILSPManagerService.ROOT_APATCH,
- rootVersion = "10763",
+ rootImplementation = IManagerService.ROOT_APATCH,
),
DemoScenario(
id = "update-available",
title = "An update is available",
summary = "Reports version 1, so a real release becomes an update. Shows the picker.",
- xposedVersionCode = 1,
+ frameworkVersionCode = 1,
),
DemoScenario(
id = "install-fails",
title = "Flash that dies halfway",
summary = "Output already streamed, then a non-zero exit. The case that bites.",
- xposedVersionCode = 1,
+ frameworkVersionCode = 1,
install = DemoScenario.InstallScript.FAILS_PARTWAY,
),
DemoScenario(
diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt
index 193249da0..a0e7964f0 100644
--- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt
+++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt
@@ -5,10 +5,11 @@ import android.content.pm.PackageInfo
import android.content.pm.ResolveInfo
import android.os.Build
import android.os.ParcelFileDescriptor
-import org.lsposed.lspd.IFrameworkInstallCallback
-import org.lsposed.lspd.ILSPManagerService
-import org.lsposed.lspd.models.Application
-import org.lsposed.lspd.models.UserInfo
+import org.matrix.vector.ipc.DeviceUser
+import org.matrix.vector.ipc.IFrameworkInstallReceiver
+import org.matrix.vector.ipc.IManagerService
+import org.matrix.vector.ipc.ModuleLoadFailure
+import org.matrix.vector.ipc.ScopeEntry
import rikka.parcelablelist.ParcelableListSlice
import org.matrix.vector.manager.data.model.versionCodeCompat
@@ -38,8 +39,8 @@ import org.matrix.vector.manager.data.model.versionCodeCompat
*/
class FakeManagerService(
private val scenario: DemoScenario,
- private val real: ILSPManagerService?,
-) : ILSPManagerService.Stub() {
+ private val real: IManagerService?,
+) : IManagerService.Stub() {
/**
* What each package's version was when the scenario started.
@@ -55,6 +56,16 @@ class FakeManagerService(
if (scenario.stallMillis > 0) Thread.sleep(scenario.stallMillis)
}
+ /**
+ * Neither scripted nor delegated.
+ *
+ * This class *is* this build's `Stub`, so the generation it answers to is the one this file was
+ * compiled against — the same answer the daemon of this build gives. Passing the real daemon's
+ * number through would report a peer's protocol for a peer that is not the one on the other end
+ * of these transactions.
+ */
+ override fun getProtocolVersion(): Int = IManagerService.PROTOCOL_VERSION
+
// ---- what the scenario exists to lie about ------------------------------------------------
override fun isSepolicyLoaded(): Boolean {
@@ -62,38 +73,37 @@ class FakeManagerService(
return scenario.sepolicyLoaded
}
- override fun systemServerRequested(): Boolean {
+ override fun isSystemServerAttached(): Boolean {
stall()
- return scenario.systemServerRequested
+ return scenario.systemServerAttached
}
- override fun dex2oatFlagsLoaded(): Boolean {
+ override fun isDex2OatInliningDisabled(): Boolean {
stall()
- return scenario.dex2oatFlagsLoaded
+ return scenario.dex2OatInliningDisabled
}
- override fun getDex2OatWrapperCompatibility(): Int = scenario.dex2oatCompatibility
+ override fun getDex2OatWrapperState(): Int = scenario.dex2OatWrapperState
- override fun getXposedApiVersion(): Int =
- scenario.xposedApiVersion.takeIf { it != DemoScenario.PASS_THROUGH }
- ?: real?.xposedApiVersion
+ override fun getLibxposedApiVersion(): Int =
+ scenario.libxposedApiVersion.takeIf { it != DemoScenario.PASS_THROUGH }
+ ?: real?.libxposedApiVersion
?: 0
- override fun getXposedVersionCode(): Long =
- scenario.xposedVersionCode.takeIf { it != DemoScenario.PASS_THROUGH.toLong() }
- ?: real?.xposedVersionCode
+ override fun getFrameworkVersionCode(): Long =
+ scenario.frameworkVersionCode.takeIf { it != DemoScenario.PASS_THROUGH.toLong() }
+ ?: real?.frameworkVersionCode
?: 0L
override fun getRootImplementation(): Int = scenario.rootImplementation
/**
- * Passed through, because a scenario that lied about the commit would be testing the *mismatch*
- * warning rather than the states this harness exists for. Add a field here when there is a
- * scenario that needs one.
+ * Passed through, because a scenario that lied about the build stamp would be testing the
+ * *mismatch* warning rather than the states this harness exists for. Add a field here when
+ * there is a scenario that needs one.
*/
- override fun getFrameworkCommit(): String? = real?.frameworkCommit
+ override fun getBuildStamp(): String? = real?.buildStamp
- override fun getRootImplementationVersion(): String? = scenario.rootVersion
/**
* A flash, without a flash.
@@ -102,16 +112,18 @@ class FakeManagerService(
* a screen that only works when the lines arrive on the binder thread would pass here and hang
* on a device.
*/
- override fun installFrameworkZip(zipPath: String?, callback: IFrameworkInstallCallback?) {
- if (callback == null) return
+ override fun installFrameworkZip(zipPath: String?, receiver: IFrameworkInstallReceiver?) {
+ if (receiver == null) return
Thread {
fun say(line: String) {
- runCatching { callback.onLine(line) }
+ runCatching { receiver.onLine(line) }
Thread.sleep(220)
}
when (scenario.install) {
DemoScenario.InstallScript.NO_ROOT -> {
- runCatching { callback.onFinished(ILSPManagerService.INSTALL_NO_ROOT) }
+ runCatching {
+ receiver.onFinished(IFrameworkInstallReceiver.INSTALL_NO_ROOT)
+ }
}
DemoScenario.InstallScript.SUCCEEDS -> {
say("- Target: $zipPath")
@@ -120,7 +132,7 @@ class FakeManagerService(
say("- Installing Vector")
say("- Setting permissions")
say("- Done. Reboot to apply.")
- runCatching { callback.onFinished(0) }
+ runCatching { receiver.onFinished(0) }
}
DemoScenario.InstallScript.FAILS_PARTWAY -> {
say("- Target: $zipPath")
@@ -128,7 +140,7 @@ class FakeManagerService(
say("- Device is arm64-v8a API 36")
say("- Installing Vector")
say("! Failed to copy zygisk binary: No space left on device")
- runCatching { callback.onFinished(1) }
+ runCatching { receiver.onFinished(1) }
}
}
}
@@ -182,35 +194,38 @@ class FakeManagerService(
return ParcelableListSlice(rewritten)
}
- override fun enabledModules(): Array = real?.enabledModules() ?: emptyArray()
+ override fun getEnabledModules(): MutableList = real?.enabledModules ?: mutableListOf()
- override fun getUnloadableModules(): Array =
- real?.unloadableModules ?: emptyArray()
-
- override fun getModuleLoadState(packageName: String?): Int =
- real?.getModuleLoadState(packageName) ?: ILSPManagerService.MODULE_LOAD_OK
-
- override fun enableModule(packageName: String?): Boolean =
- real?.enableModule(packageName) ?: false
+ /**
+ * The empty list is the whole answer for a device with nothing wrong: a module absent from it
+ * loaded, so no daemon means nothing to report rather than a state to invent.
+ */
+ override fun getModuleLoadFailures(): MutableList =
+ real?.moduleLoadFailures ?: mutableListOf()
- override fun disableModule(packageName: String?): Boolean =
- real?.disableModule(packageName) ?: false
+ override fun setModuleEnabled(packageName: String?, enabled: Boolean): Boolean =
+ real?.setModuleEnabled(packageName, enabled) ?: false
- override fun setModuleScope(packageName: String?, scope: MutableList?): Boolean =
+ override fun setModuleScope(packageName: String?, scope: MutableList?): Boolean =
real?.setModuleScope(packageName, scope) ?: false
- override fun getModuleScope(packageName: String?): MutableList =
- real?.getModuleScope(packageName) ?: mutableListOf()
+ /**
+ * Null is handed on rather than flattened, because the daemon answers it only for the
+ * framework's own pseudo-module row, which is not the same answer as a module with nothing
+ * scoped to it — and a fake that collapsed the two would hide a refusal from the very code this
+ * demo exists to exercise. The empty list is the no-daemon answer alone.
+ */
+ override fun getModuleScope(packageName: String?): MutableList? =
+ if (real == null) mutableListOf() else real.getModuleScope(packageName)
- override fun isVerboseLog(): Boolean = real?.isVerboseLog ?: false
+ override fun isVerboseLogEnabled(): Boolean = real?.isVerboseLogEnabled ?: false
- override fun setVerboseLog(enabled: Boolean) {
- real?.setVerboseLog(enabled)
+ override fun setVerboseLogEnabled(enabled: Boolean) {
+ real?.setVerboseLogEnabled(enabled)
}
- override fun getVerboseLog(): ParcelFileDescriptor? = real?.verboseLog
-
- override fun getModulesLog(): ParcelFileDescriptor? = real?.modulesLog
+ override fun getLiveLogPart(verbose: Boolean): ParcelFileDescriptor? =
+ real?.getLiveLogPart(verbose)
override fun getLogParts(verbose: Boolean): MutableList =
real?.getLogParts(verbose) ?: mutableListOf()
@@ -226,12 +241,11 @@ class FakeManagerService(
*/
override fun getManagerApk(): ParcelFileDescriptor? = real?.managerApk
- override fun getXposedVersionName(): String? = real?.xposedVersionName
-
- override fun clearLogs(verbose: Boolean): Boolean = real?.clearLogs(verbose) ?: false
+ override fun getFrameworkVersionName(): String? = real?.frameworkVersionName
- override fun getPackageInfo(packageName: String?, flags: Int, uid: Int): PackageInfo? =
- real?.getPackageInfo(packageName, flags, uid)
+ override fun startNewLogPart(verbose: Boolean) {
+ real?.startNewLogPart(verbose)
+ }
override fun forceStopPackage(packageName: String?, userId: Int) {
real?.forceStopPackage(packageName, userId)
@@ -243,13 +257,12 @@ class FakeManagerService(
override fun uninstallPackage(packageName: String?, userId: Int): Boolean =
real?.uninstallPackage(packageName, userId) ?: false
- override fun getUsers(): MutableList = real?.users ?: mutableListOf()
-
- override fun installExistingPackageAsUser(packageName: String?, userId: Int): Int =
- real?.installExistingPackageAsUser(packageName, userId) ?: 0
+ override fun getUsers(): MutableList = real?.users ?: mutableListOf()
- override fun startActivityAsUserWithFeature(intent: Intent?, userId: Int): Int =
- real?.startActivityAsUserWithFeature(intent, userId) ?: 0
+ override fun startActivityAsUser(intent: Intent?, userId: Int, noUserSwitch: Boolean): Int =
+ // -1, not 0: the AIDL documents 0..99 as "the activity started", so a benign-looking
+ // 0 would report a successful start with no daemon behind it.
+ real?.startActivityAsUser(intent, userId, noUserSwitch) ?: -1
override fun queryIntentActivitiesAsUser(
intent: Intent?,
@@ -263,32 +276,28 @@ class FakeManagerService(
// with it, and every screen this scenario exists to show would go with it.
}
- override fun forcedLauncherIcons(): Boolean = real?.forcedLauncherIcons() ?: true
+ override fun isForcedLauncherIcons(): Boolean = real?.isForcedLauncherIcons ?: true
override fun setForcedLauncherIcons(force: Boolean) {
real?.setForcedLauncherIcons(force)
}
- override fun getLogs(zipFd: ParcelFileDescriptor?) {
- real?.getLogs(zipFd)
- }
-
- override fun restartFor(intent: Intent?) {
- real?.restartFor(intent)
+ override fun writeBugReport(zipFd: ParcelFileDescriptor?) {
+ real?.writeBugReport(zipFd)
}
override fun optimizePackage(packageName: String?): Boolean =
real?.optimizePackage(packageName) ?: false
// `?: true` to match the daemon, whose PreferenceStore reads this one `?: true` when nobody has
- // set it — the same reason forcedLauncherIcons above answers true. A fallback here is not a
+ // set it — the same reason isForcedLauncherIcons above answers true. A fallback here is not a
// failed read: it is handed upstream as a *successful* answer, so answering false would leave
// the status page's switch — and the ManagerPresence field HomeViewModel fills from the same
// call — showing the opposite of what an untouched device with a real daemon behind it says.
- override fun enableStatusNotification(): Boolean = real?.enableStatusNotification() ?: true
+ override fun isStatusNotificationEnabled(): Boolean = real?.isStatusNotificationEnabled ?: true
- override fun setEnableStatusNotification(enable: Boolean) {
- real?.setEnableStatusNotification(enable)
+ override fun setStatusNotificationEnabled(enabled: Boolean) {
+ real?.setStatusNotificationEnabled(enabled)
}
override fun getIncludeNewApps(packageName: String?): Boolean =
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt b/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt
index b0d120726..89d09ea24 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt
@@ -2,7 +2,7 @@ package org.matrix.vector.manager
import android.os.IBinder
import kotlin.system.exitProcess
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.manager.di.ServiceLocator
/**
@@ -29,7 +29,52 @@ object Constants {
@JvmStatic
fun setBinder(binder: IBinder): Boolean {
- ServiceLocator.bind(ILSPManagerService.Stub.asInterface(binder))
+ // The interface's fully qualified name is its binder descriptor, and this APK can be older
+ // or newer than the framework that pushed the binder: `getManagerApk` exists so the manager
+ // can be installed as an ordinary app, and an installed copy survives every later flash.
+ //
+ // Nothing about that mismatch is loud on its own. `Stub.asInterface` wraps any binder in a
+ // proxy without checking, the binder stays alive so `isBinderAlive()` keeps answering true,
+ // and every transaction then throws SecurityException out of the daemon's
+ // `enforceInterface` — which `DaemonClient.runIpc` turns into a failed Result and every
+ // screen draws as empty. So ask first.
+ //
+ // This one question is exempt by construction: INTERFACE_TRANSACTION sits outside the
+ // FIRST_CALL_TRANSACTION..LAST_CALL_TRANSACTION band the generated dispatcher checks the
+ // token for, so it is answered across any mismatch. It can still throw — the call is remote
+ // and the daemon may have died between the push and here — and a throw is not evidence of a
+ // mismatch, so it falls through to binding and lets linkToDeath below report the death.
+ val theirDescriptor = runCatching { binder.interfaceDescriptor }.getOrNull()
+ if (theirDescriptor != null && theirDescriptor != IManagerService.DESCRIPTOR) {
+ logE(
+ "ipc: the daemon speaks $theirDescriptor, this manager speaks " +
+ "${IManagerService.DESCRIPTOR}; refusing to bind"
+ )
+ ServiceLocator.bindMismatch(theirDescriptor)
+ return false
+ }
+
+ val service = IManagerService.Stub.asInterface(binder)
+
+ // A matching descriptor means the two ends agree on what this interface is called, not on
+ // what is in it. Transaction ids follow declaration order, so a daemon built from a
+ // different revision of the AIDL maps the same numbers to different methods, and every call
+ // would land somewhere plausible and wrong -- which is worse than failing, because nothing
+ // throws. getProtocolVersion is declared first and is therefore transaction zero in every
+ // revision, so it is the one question both ends are guaranteed to agree on. A daemon too
+ // old to implement it answers 0 out of an untouched reply parcel, which is below the floor
+ // and refused for the right reason.
+ val theirProtocol = runCatching { service.protocolVersion }.getOrNull()
+ if (theirProtocol != null && theirProtocol != IManagerService.PROTOCOL_VERSION) {
+ logE(
+ "ipc: the daemon speaks protocol $theirProtocol, this manager speaks " +
+ "${IManagerService.PROTOCOL_VERSION}; refusing to bind"
+ )
+ ServiceLocator.bindMismatch("protocol $theirProtocol")
+ return false
+ }
+
+ ServiceLocator.bind(service)
try {
// If the daemon dies the manager is holding a dead binder and every screen would
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashReport.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashReport.kt
index 02574f19b..983864831 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashReport.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashReport.kt
@@ -95,6 +95,9 @@ data class CrashFrame(
private val OUR_PACKAGES =
listOf(
"org.matrix.vector",
+ // Where this project's own code used to live. A trace is read long after it was captured —
+ // out of a saved report, or out of the crash cache an update did not clear — so the frames
+ // an older build wrote still arrive under the old name and are still ours.
"org.lsposed.lspd",
"de.robv.android.xposed",
"io.github.libxposed",
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/BuildStamp.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/BuildStamp.kt
index aa19cfc3e..5cad24fee 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/BuildStamp.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/BuildStamp.kt
@@ -4,7 +4,7 @@ package org.matrix.vector.manager.data.model
* A build stamp, taken apart: which commit a build came from, and where it was built.
*
* The framework and the manager both report one — `BuildConfig.VERSION_HASH`, and
- * `getFrameworkCommit()` across the binder — because the version code cannot tell two builds apart:
+ * `getBuildStamp()` across the binder — because the version code cannot tell two builds apart:
* it is the commit count on origin/master, so every branch build at the same depth wears the same
* number as the official build it was never made from.
*
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt
index 091985e2b..0c497a2f9 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt
@@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
-import org.lsposed.lspd.models.Application
+import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.manager.data.log.archiveBuildStamp
import org.matrix.vector.manager.ipc.DaemonClient
import org.matrix.vector.manager.logE
@@ -136,7 +136,7 @@ class BackupRepository(private val context: Context, private val daemon: DaemonC
if (module.scope.isNotEmpty()) {
val scope =
module.scope.map { target ->
- Application().apply {
+ ScopeEntry().apply {
packageName = target.packageName
userId = target.userId
}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkInstaller.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkInstaller.kt
index 2b39fe6a3..705fc3234 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkInstaller.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkInstaller.kt
@@ -18,8 +18,7 @@ import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
-import org.lsposed.lspd.IFrameworkInstallCallback
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IFrameworkInstallReceiver
import org.matrix.vector.manager.ipc.DaemonClient
import org.matrix.vector.manager.logE
import org.matrix.vector.manager.logW
@@ -37,7 +36,7 @@ sealed interface FlashStep {
/** The installer exited zero. A reboot is what makes it take effect. */
data object Done : FlashStep
- /** [code] is the installer's exit status, or one of ILSPManagerService.INSTALL_*. */
+ /** [code] is the installer's exit status, or one of IFrameworkInstallReceiver.INSTALL_*. */
data class Failed(val code: Int) : FlashStep
}
@@ -135,7 +134,7 @@ class FrameworkInstaller(
.getOrElse { e ->
logW("update: unusable download url $url", e)
append("Download failed: ${e.message}")
- _state.value = FlashStep.Failed(ILSPManagerService.INSTALL_NO_SUCH_FILE)
+ _state.value = FlashStep.Failed(IFrameworkInstallReceiver.INSTALL_NO_SUCH_FILE)
return
}
transfer = call
@@ -227,7 +226,7 @@ class FrameworkInstaller(
if (e is CancellationException) throw e
logW("update: download failed", e)
append("Download failed: ${e.message}")
- _state.value = FlashStep.Failed(ILSPManagerService.INSTALL_NO_SUCH_FILE)
+ _state.value = FlashStep.Failed(IFrameworkInstallReceiver.INSTALL_NO_SUCH_FILE)
return
}
@@ -292,15 +291,15 @@ class FrameworkInstaller(
/**
* Runs the daemon-side install and suspends until it reports an exit code.
*
- * The installer's output arrives on the callback as it is produced rather than with the result,
+ * The installer's output arrives on the receiver as it is produced rather than with the result,
* so the screen fills in during a flash that takes minutes. The exit code comes separately, on
* a deferred nobody here abandons: it is the one moment the flash can be called finished, and a
* wait that ended early left the bar spinning over an install that had long since succeeded.
*/
private suspend fun awaitInstall(path: String) {
val done = kotlinx.coroutines.CompletableDeferred()
- val callback =
- object : IFrameworkInstallCallback.Stub() {
+ val receiver =
+ object : IFrameworkInstallReceiver.Stub() {
override fun onLine(line: String?) {
line?.let(::append)
}
@@ -310,12 +309,12 @@ class FrameworkInstaller(
}
}
- val started = daemon.installFrameworkZip(path, callback)
+ val started = daemon.installFrameworkZip(path, receiver)
if (started.isFailure) {
val cause = started.exceptionOrNull()
logE("update: daemon did not start the install of $path", cause)
append("The daemon refused the install: ${cause?.message}")
- _state.value = FlashStep.Failed(ILSPManagerService.INSTALL_NOT_EXECUTED)
+ _state.value = FlashStep.Failed(IFrameworkInstallReceiver.INSTALL_NOT_EXECUTED)
return
}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/ManagerInstaller.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/ManagerInstaller.kt
index 909544074..b2b9a489c 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/ManagerInstaller.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/ManagerInstaller.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.manager.BuildConfig
import org.matrix.vector.manager.data.model.ManagerCopy
import org.matrix.vector.manager.data.model.versionCodeCompat
@@ -111,7 +112,9 @@ class ManagerInstaller(private val context: Context, private val daemon: DaemonC
*/
suspend fun removeConflicting(): Boolean {
val removed =
- daemon.uninstallPackage(BuildConfig.MANAGER_PACKAGE_NAME, ALL_USERS).getOrDefault(false)
+ daemon
+ .uninstallPackage(BuildConfig.MANAGER_PACKAGE_NAME, IManagerService.ALL_USERS)
+ .getOrDefault(false)
if (removed) _state.value = ManagerInstallStep.Idle
else logW("actions: could not remove the conflicting manager")
return removed
@@ -378,8 +381,5 @@ class ManagerInstaller(private val context: Context, private val daemon: DaemonC
* the life of the process, which is exactly what it did.
*/
const val APK_TIMEOUT_MS = 30_000L
-
- /** `ManagerService.uninstallPackage` reads -1 as "every user". */
- const val ALL_USERS = -1
}
}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt
index fe1c59acc..b67f65b27 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt
@@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.manager.data.log.CrashRecorder
import org.matrix.vector.manager.data.github.GitHubRepository
import org.matrix.vector.manager.data.repository.AppRepository
@@ -66,7 +66,7 @@ object ServiceLocator {
@Volatile private var appContext: Context? = null
- private val _service = MutableStateFlow(null)
+ private val _service = MutableStateFlow(null)
/**
* The daemon binder, as observable state.
@@ -75,7 +75,24 @@ object ServiceLocator {
* they were constructed — or arrives again after a reconnect — makes them re-read instead of
* leaving them with whatever they managed to fetch before there was a daemon at all.
*/
- val service: StateFlow = _service.asStateFlow()
+ val service: StateFlow = _service.asStateFlow()
+
+ private val _peerMismatch = MutableStateFlow(null)
+
+ /**
+ * What the daemon turned out to be, when it is not something this build can talk to.
+ *
+ * Null in the ordinary case, including "no daemon at all" — this is only ever set when a binder
+ * did arrive and was then refused. That is a distinct situation from having no daemon and has to
+ * be rendered as one: the binder is alive and the framework is plainly running, so every screen
+ * would otherwise draw it as one that answers nothing.
+ *
+ * Two things set it, and the string says which: a descriptor that is not this interface, or a
+ * protocol version this build does not speak. It is text for a log and a marker for the header
+ * rather than something to branch on — the answer to both is the same, and a reader cannot act
+ * on the difference.
+ */
+ val peerMismatch: StateFlow = _peerMismatch.asStateFlow()
val context: Context
get() =
@@ -281,7 +298,20 @@ object ServiceLocator {
}
/** Called from `Constants.setBinder`, possibly before [attach]. */
- fun bind(service: ILSPManagerService?) {
+ fun bind(service: IManagerService?) {
_service.value = service
+ _peerMismatch.value = null
+ }
+
+ /**
+ * Called instead of [bind] when the binder that arrived is not one this build can use.
+ *
+ * Deliberately leaves [service] null. A refused peer is not a degraded daemon that answers some
+ * calls; it is one whose every answer would be thrown or wrong, so handing it out would only
+ * spread failures across every screen.
+ */
+ fun bindMismatch(what: String) {
+ _service.value = null
+ _peerMismatch.value = what
}
}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt
index c2265bff0..b4c0705f8 100644
--- a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt
@@ -3,10 +3,10 @@ package org.matrix.vector.manager.ipc
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
-import org.lsposed.lspd.IFrameworkInstallCallback
+import org.matrix.vector.ipc.IFrameworkInstallReceiver
import android.content.Intent
import android.content.pm.ActivityInfo
-import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.manager.logE
import org.matrix.vector.manager.logW
@@ -16,9 +16,9 @@ import org.matrix.vector.manager.logW
* A Binder transaction is synchronous and the daemon on the other end can be slow, busy or gone, so
* none of this is allowed to happen on the thread that draws.
*/
-class DaemonClient(private val serviceState: StateFlow) {
+class DaemonClient(private val serviceState: StateFlow) {
- val service: ILSPManagerService?
+ val service: IManagerService?
get() = serviceState.value
val isAlive: Boolean
@@ -29,7 +29,7 @@ class DaemonClient(private val serviceState: StateFlow) {
* unreachable or refusing daemon is a value the caller can render rather than a thrown
* exception.
*/
- private suspend fun runIpc(block: (ILSPManagerService) -> T): Result =
+ private suspend fun runIpc(block: (IManagerService) -> T): Result =
withContext(Dispatchers.IO) {
// Read the binder once: it comes from a StateFlow the daemon can change underneath us,
// so checking one value for liveness and calling another is a race with the daemon
@@ -50,9 +50,9 @@ class DaemonClient(private val serviceState: StateFlow) {
}
}
- suspend fun getXposedApiVersion(): Result = runIpc { it.xposedApiVersion }
+ suspend fun getLibxposedApiVersion(): Result = runIpc { it.libxposedApiVersion }
- suspend fun getEnabledModules(): Result> = runIpc { it.enabledModules().toList()
+ suspend fun getEnabledModules(): Result> = runIpc { it.enabledModules
}
/**
@@ -98,12 +98,9 @@ class DaemonClient(private val serviceState: StateFlow) {
/**
* Opens that screen.
*
- * The `lsp_no_switch_to_user` extra is not decoration. Without it, and whenever the current
- * user is not already the target's profile parent, the daemon switches the device to that
- * parent and locks the screen before starting the activity — right for an activity that exists
- * in one profile only, and a startling thing to do to someone who pressed "open" on a module
- * whose window shows for every user anyway. The flag on the resolved activity says which case
- * this is.
+ * Whether to suppress the daemon's user switch is decided here, from the resolved activity's
+ * `FLAG_SHOW_FOR_ALL_USERS` — see [startActivityAsUser] for what the switch does and why an
+ * activity that shows for every user must not trigger one.
*
* Returns false when the package has no such screen, which is an answer rather than a failure.
*/
@@ -125,15 +122,12 @@ class DaemonClient(private val serviceState: StateFlow) {
}
return runIpc { service ->
val code =
- service.startActivityAsUserWithFeature(
+ service.startActivityAsUser(
Intent(Intent.ACTION_MAIN)
.setClassName(target.packageName, target.name)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(
- "lsp_no_switch_to_user",
- (target.flags and FLAG_SHOW_FOR_ALL_USERS) != 0,
- ),
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
userId,
+ (target.flags and FLAG_SHOW_FOR_ALL_USERS) != 0,
)
// The daemon hands back the activity manager's own start code, so a refusal reaches the
// caller rather than a flat `true`: a refused user switch (-1), a disabled or
@@ -156,35 +150,32 @@ class DaemonClient(private val serviceState: StateFlow) {
}
/**
- * Modules the daemon could not load, though they are installed and enabled.
+ * Modules the daemon could not load, though they are installed and enabled, keyed by package
+ * with one of `IManagerService.MODULE_LOAD_*` as the value.
*
* The daemon keeps what the user asked for separately from what it can actually load, and the
* two can disagree — an APK whose path will not resolve, a DEX the loader refuses. A module
* listed here is still switched on; it is the loading that failed, and saying so is the only
* way the screen can tell that apart from "switched off".
+ *
+ * A map because that is what the daemon holds and what the caller wants. It used to be a list
+ * of names plus one transaction per name to ask why, which forced the caller to seed each
+ * entry with a placeholder reason before the second round could overwrite it — so a dropped
+ * transaction reported a module as missing its APK, which nothing had established.
*/
- suspend fun getUnloadableModules(): Result> = runIpc {
- it.unloadableModules.toList()
- }
-
- /** Why a module in that list could not be loaded; `MODULE_LOAD_OK` when it is fine. */
- suspend fun getModuleLoadState(packageName: String): Result = runIpc {
- it.getModuleLoadState(packageName)
+ suspend fun getModuleLoadFailures(): Result
+ *
+ * Transaction ids are implicit, as in {@link IFrameworkService}. This binder reaches an
+ * injected process inside a {@link LoadedModule}, so it only ever crosses between a daemon and a
+ * framework dex from the same zip. Append new methods; do not insert.
*/
interface IModuleService {
/** The framework capability bits, as {@code XposedInterface#getFrameworkProperties}. */
diff --git a/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IVectorDaemon.aidl b/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IVectorDaemon.aidl
index b969c1e31..b449f1362 100644
--- a/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IVectorDaemon.aidl
+++ b/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IVectorDaemon.aidl
@@ -8,13 +8,23 @@ import org.matrix.vector.ipc.IFrameworkService;
* This binder is pushed into a process by the daemon rather than looked up: the zygisk native
* module intercepts {@code Binder.execTransact}, and the daemon transacts a magic code on any
* binder to hand it over. Nothing here is registered in servicemanager.
+ *
+ * Transaction ids are implicit, as in {@link IFrameworkService}, and for the same reason.
+ * The daemon and the zygisk bridge that calls this ship in one zip, so they are always the same
+ * build and nothing has to survive a peer of a different revision. A new method must still be
+ * appended: inserting one anywhere above renumbers every method after it. Contrast
+ * {@code IManagerService}, whose ids are explicit because the manager APK can be installed
+ * separately and outlive a flash.
*/
interface IVectorDaemon {
/**
* Announces a process to the daemon and asks for its framework service.
*
- * Returns null when no module is in scope for this process, which is the ordinary answer for
- * most of them.
+ * Null is the ordinary answer and covers four cases the caller cannot tell apart, because
+ * there is nothing it could do differently about any of them: the call did not come from
+ * uid 1000 and was refused; this (uid, pid) has already attached; no module is in scope for
+ * this process, which is most of them; or the registration itself failed. A process that is
+ * answered null simply carries on unhooked.
*
* @param uid the process uid, as the daemon will re-derive it from the binder call
* @param pid the process id
@@ -25,7 +35,8 @@ interface IVectorDaemon {
* caller must keep a strong reference to it (the native side takes a JNI
* global ref); letting it be collected looks exactly like dying.
*/
- IFrameworkService attachProcess(int uid, int pid, String processName, IBinder processLifeToken);
+ @nullable IFrameworkService attachProcess(int uid, int pid, String processName,
+ IBinder processLifeToken);
/** Gives the daemon system_server's ActivityThread and activity token, once they exist. */
oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken);
diff --git a/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/LoadedModule.aidl b/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/LoadedModule.aidl
index 9a7f0a1d5..d324c67d5 100644
--- a/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/LoadedModule.aidl
+++ b/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/LoadedModule.aidl
@@ -37,6 +37,15 @@ parcelable LoadedModule {
/** The generation of code to load. */
ModuleCode code;
+ /**
+ * The module app's own ApplicationInfo, as PackageManager reported it to the daemon.
+ *
+ * Carried rather than looked up because the process receiving it usually cannot: it runs as
+ * the app it was injected into, and system_server is served before PackageManager is published
+ * at all. Reaches the module as {@code XposedInterface#getModuleApplicationInfo}, whose name
+ * carries the fact worth keeping: it describes the module and never the process it is
+ * running in.
+ */
ApplicationInfo applicationInfo;
/** What {@code XposedInterface}'s remote preferences and remote files calls go through. */
diff --git a/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java b/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java
deleted file mode 100644
index 0aca992b1..000000000
--- a/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.lspd.util;
-
-import android.os.SystemProperties;
-import android.text.TextUtils;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-public class Utils {
-
- public static final String LOG_TAG = "Vector";
- public static final boolean isMIUI = !TextUtils.isEmpty(SystemProperties.get("ro.miui.ui.version.name"));
-
- public class Log {
- public static final int VERBOSE = android.util.Log.VERBOSE;
- public static final int DEBUG = android.util.Log.DEBUG;
- public static final int INFO = android.util.Log.INFO;
- public static final int WARN = android.util.Log.WARN;
- public static final int ERROR = android.util.Log.ERROR;
- public static final int ASSERT = android.util.Log.ASSERT;
-
- public static boolean muted = false;
-
- public static void println(int priority, String tag, String msg) {
- // Respect the muted flag for everything except ERROR/ASSERT
- if (muted && priority < android.util.Log.ERROR) return;
- android.util.Log.println(priority, tag, msg);
- }
-
- /**
- * A throwable as text, without the platform's filtering.
- *
- * {@code android.util.Log.getStackTraceString} returns an empty string when anything in
- * the cause chain is an {@link java.net.UnknownHostException} — deliberately upstream, to
- * cut log spew when the network is down, but here it silently turns a module's report of a
- * failed request into a message with nothing under it.
- */
- public static String getStackTraceString(Throwable tr) {
- if (tr == null) return "";
- StringWriter sw = new StringWriter();
- tr.printStackTrace(new PrintWriter(sw));
- return sw.toString().stripTrailing();
- }
-
- public static void d(String tag, String msg) {
- if (muted) return;
- android.util.Log.d(tag, msg);
- }
-
- public static void d(String tag, String msg, Throwable tr) {
- android.util.Log.d(tag, msg, tr);
- }
-
- public static void v(String tag, String msg) {
- if (muted) return;
- android.util.Log.v(tag, msg);
- }
-
- public static void v(String tag, String msg, Throwable tr) {
- android.util.Log.v(tag, msg, tr);
- }
-
- public static void i(String tag, String msg) {
- if (muted) return;
- android.util.Log.i(tag, msg);
- }
-
- public static void i(String tag, String msg, Throwable tr) {
- android.util.Log.i(tag, msg, tr);
- }
-
- public static void w(String tag, String msg) {
- if (muted) return;
- android.util.Log.w(tag, msg);
- }
-
- public static void w(String tag, String msg, Throwable tr) {
- if (muted) return;
- android.util.Log.w(tag, msg, tr);
- }
-
- public static void e(String tag, String msg) {
- android.util.Log.e(tag, msg);
- }
-
- public static void e(String tag, String msg, Throwable tr) {
- android.util.Log.e(tag, msg, tr);
- }
-
-
- }
-
- public static void logD(Object msg) {
- Log.d(LOG_TAG, msg.toString());
- }
-
- public static void logD(String msg, Throwable throwable) {
- Log.d(LOG_TAG, msg, throwable);
- }
-
- public static void logV(Object msg) {
- Log.v(LOG_TAG, msg.toString());
- }
-
- public static void logV(String msg, Throwable throwable) {
- Log.v(LOG_TAG, msg, throwable);
- }
-
- public static void logW(String msg) {
- Log.w(LOG_TAG, msg);
- }
-
- public static void logW(String msg, Throwable throwable) {
- Log.w(LOG_TAG, msg, throwable);
- }
-
- public static void logI(String msg) {
- Log.i(LOG_TAG, msg);
- }
-
- public static void logI(String msg, Throwable throwable) {
- Log.i(LOG_TAG, msg, throwable);
- }
-
- public static void logE(String msg) {
- Log.e(LOG_TAG, msg);
- }
-
- public static void logE(String msg, Throwable throwable) {
- Log.e(LOG_TAG, msg, throwable);
- }
-}
diff --git a/services/daemon-service/src/main/java/org/matrix/vector/util/Log.java b/services/daemon-service/src/main/java/org/matrix/vector/util/Log.java
new file mode 100644
index 000000000..9ffd4cfad
--- /dev/null
+++ b/services/daemon-service/src/main/java/org/matrix/vector/util/Log.java
@@ -0,0 +1,111 @@
+package org.matrix.vector.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * A drop-in replacement for {@code android.util.Log} that the user can silence.
+ *
+ * Written to be substitutable by import alone for the ten overloads it covers: each takes the
+ * arguments its {@code android.util.Log} counterpart takes, so a file switches over by changing
+ * which {@code Log} it imports. That is why it keeps the platform's terse names. It is not a
+ * complete stand-in — these return void where the platform returns the number of bytes written, and
+ * {@code wtf}, {@code isLoggable} and the {@code (String, Throwable)} overloads are absent — so a
+ * file that uses any of those has to keep reaching for the platform's.
+ *
+ * Lives here rather than as a member of {@link Utils}, where it began. It held only static
+ * members while being a non-static inner class, which Java accepts only from release 16 and which
+ * meant every call site had to write {@code Utils.Log} for a type that was never scoped to an
+ * instance of anything.
+ */
+public class Log {
+ public static final int VERBOSE = android.util.Log.VERBOSE;
+ public static final int DEBUG = android.util.Log.DEBUG;
+ public static final int INFO = android.util.Log.INFO;
+ public static final int WARN = android.util.Log.WARN;
+ public static final int ERROR = android.util.Log.ERROR;
+ public static final int ASSERT = android.util.Log.ASSERT;
+
+ /**
+ * Whether the user has asked the framework to keep quiet.
+ *
+ * Set in an injected process from {@code IFrameworkService.isLogMuted}, and deliberately not
+ * consulted for {@link #e} or for anything at {@code ERROR} and above: muting is a request for
+ * less noise, not for a failure to go unrecorded.
+ *
+ * Everything below that is gated, the {@code Throwable} overloads included. They were not,
+ * which made the setting only half work: a user who turned verbose logging off still got every
+ * debug, verbose and info line that happened to carry an exception, and those are the ones in
+ * the hot paths of an injected process.
+ */
+ public static boolean muted = false;
+
+ public static void println(int priority, String tag, String msg) {
+ // Respect the muted flag for everything except ERROR/ASSERT
+ if (muted && priority < android.util.Log.ERROR) return;
+ android.util.Log.println(priority, tag, msg);
+ }
+
+ /**
+ * A throwable as text, without the platform's filtering.
+ *
+ * {@code android.util.Log.getStackTraceString} returns an empty string when anything in
+ * the cause chain is an {@link java.net.UnknownHostException} — deliberately upstream, to
+ * cut log spew when the network is down, but here it silently turns a module's report of a
+ * failed request into a message with nothing under it.
+ */
+ public static String getStackTraceString(Throwable tr) {
+ if (tr == null) return "";
+ StringWriter sw = new StringWriter();
+ tr.printStackTrace(new PrintWriter(sw));
+ return sw.toString().stripTrailing();
+ }
+
+ public static void d(String tag, String msg) {
+ if (muted) return;
+ android.util.Log.d(tag, msg);
+ }
+
+ public static void d(String tag, String msg, Throwable tr) {
+ if (muted) return;
+ android.util.Log.d(tag, msg, tr);
+ }
+
+ public static void v(String tag, String msg) {
+ if (muted) return;
+ android.util.Log.v(tag, msg);
+ }
+
+ public static void v(String tag, String msg, Throwable tr) {
+ if (muted) return;
+ android.util.Log.v(tag, msg, tr);
+ }
+
+ public static void i(String tag, String msg) {
+ if (muted) return;
+ android.util.Log.i(tag, msg);
+ }
+
+ public static void i(String tag, String msg, Throwable tr) {
+ if (muted) return;
+ android.util.Log.i(tag, msg, tr);
+ }
+
+ public static void w(String tag, String msg) {
+ if (muted) return;
+ android.util.Log.w(tag, msg);
+ }
+
+ public static void w(String tag, String msg, Throwable tr) {
+ if (muted) return;
+ android.util.Log.w(tag, msg, tr);
+ }
+
+ public static void e(String tag, String msg) {
+ android.util.Log.e(tag, msg);
+ }
+
+ public static void e(String tag, String msg, Throwable tr) {
+ android.util.Log.e(tag, msg, tr);
+ }
+}
diff --git a/services/daemon-service/src/main/java/org/matrix/vector/util/Utils.java b/services/daemon-service/src/main/java/org/matrix/vector/util/Utils.java
new file mode 100644
index 000000000..4cb154c80
--- /dev/null
+++ b/services/daemon-service/src/main/java/org/matrix/vector/util/Utils.java
@@ -0,0 +1,67 @@
+package org.matrix.vector.util;
+
+import android.os.SystemProperties;
+import android.text.TextUtils;
+
+/**
+ * Logging under the framework's own tag, for the code that runs inside an injected process.
+ *
+ * Use {@link Log} directly where a file has a tag of its own to log under; use the helpers here
+ * where it does not, which is most of the framework.
+ */
+public class Utils {
+
+ /**
+ * The tag every one of these helpers logs under, and it is not arbitrary.
+ *
+ * The daemon's log reader routes any tag beginning {@code Vector} into its verbose stream —
+ * see {@code kPrefixTags} in {@code daemon/src/main/jni/logcat.cpp} — so what is logged here
+ * reaches the manager's Verbose tab and travels in an exported bug report. A tag invented here
+ * that does not start with it is captured only if it is added to that reader's lists first.
+ */
+ public static final String LOG_TAG = "Vector";
+
+ /** Whether this is a MIUI/HyperOS build, which needs its own deopt workaround. */
+ public static final boolean isMIUI =
+ !TextUtils.isEmpty(SystemProperties.get("ro.miui.ui.version.name"));
+
+ public static void logD(Object msg) {
+ Log.d(LOG_TAG, msg.toString());
+ }
+
+ public static void logD(String msg, Throwable throwable) {
+ Log.d(LOG_TAG, msg, throwable);
+ }
+
+ public static void logV(Object msg) {
+ Log.v(LOG_TAG, msg.toString());
+ }
+
+ public static void logV(String msg, Throwable throwable) {
+ Log.v(LOG_TAG, msg, throwable);
+ }
+
+ public static void logW(String msg) {
+ Log.w(LOG_TAG, msg);
+ }
+
+ public static void logW(String msg, Throwable throwable) {
+ Log.w(LOG_TAG, msg, throwable);
+ }
+
+ public static void logI(String msg) {
+ Log.i(LOG_TAG, msg);
+ }
+
+ public static void logI(String msg, Throwable throwable) {
+ Log.i(LOG_TAG, msg, throwable);
+ }
+
+ public static void logE(String msg) {
+ Log.e(LOG_TAG, msg);
+ }
+
+ public static void logE(String msg, Throwable throwable) {
+ Log.e(LOG_TAG, msg, throwable);
+ }
+}
diff --git a/services/manager-service/build.gradle.kts b/services/manager-service/build.gradle.kts
index 1edbe01cc..de3a6dd92 100644
--- a/services/manager-service/build.gradle.kts
+++ b/services/manager-service/build.gradle.kts
@@ -5,7 +5,7 @@ android {
buildTypes { release { isMinifyEnabled = false } }
- namespace = "org.lsposed.lspd.managerservice"
+ namespace = "org.matrix.vector.managerservice"
}
dependencies { api(libs.rikkax.parcelablelist) }
diff --git a/services/manager-service/src/main/AndroidManifest.xml b/services/manager-service/src/main/AndroidManifest.xml
index b3a8e6f49..8072ee00d 100644
--- a/services/manager-service/src/main/AndroidManifest.xml
+++ b/services/manager-service/src/main/AndroidManifest.xml
@@ -1,21 +1,2 @@
-
-
diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/IFrameworkInstallCallback.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/IFrameworkInstallCallback.aidl
deleted file mode 100644
index 6e025c072..000000000
--- a/services/manager-service/src/main/aidl/org/lsposed/lspd/IFrameworkInstallCallback.aidl
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.lsposed.lspd;
-
-/**
- * Progress of a framework flash, one line at a time.
- *
- * `oneway` throughout: the daemon must never block on the manager while an installer is running.
- * The manager is a UI process that can be killed, paused or simply slow, and a flash that stalls
- * because nobody read a line would be a flash abandoned halfway through — with the module tree in
- * whatever state the installer had reached.
- */
-oneway interface IFrameworkInstallCallback {
-
- /** One line of the installer's combined stdout and stderr, without its trailing newline. */
- void onLine(String line);
-
- /**
- * The installer exited.
- *
- * [exitCode] is the process's own status, or a negative value when it could not be started at
- * all — see ILSPManagerService.INSTALL_* for those.
- */
- void onFinished(int exitCode);
-}
diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl
deleted file mode 100644
index 15ab06767..000000000
--- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl
+++ /dev/null
@@ -1,224 +0,0 @@
-package org.lsposed.lspd;
-
-import rikka.parcelablelist.ParcelableListSlice;
-import org.lsposed.lspd.models.UserInfo;
-import org.lsposed.lspd.models.Application;
-import org.lsposed.lspd.IFrameworkInstallCallback;
-
-
-interface ILSPManagerService {
- const int DEX2OAT_OK = 0;
- const int DEX2OAT_CRASHED = 1;
- const int DEX2OAT_MOUNT_FAILED = 2;
- const int DEX2OAT_SELINUX_PERMISSIVE = 3;
- const int DEX2OAT_SEPOLICY_INCORRECT = 4;
-
- /**
- * Which root implementation is managing this device.
- *
- * The failure values are kept apart rather than collapsed into one "unsupported", because they
- * need different sentences from the manager: nothing is installed, what is installed is too
- * old to flash through, or two implementations are fighting and flashing through either would
- * be a guess. NeoZygisk draws exactly these distinctions, and the manager is reporting on the
- * same device state.
- *
- * ROOT_UNKNOWN takes 0 because 0 is also what a binder proxy hands back for a transaction the
- * daemon does not implement. ROOT_NONE used to sit there, so a daemon too old to answer was
- * read as "no root installed", and the manager told a rooted user to go and install the root
- * manager they were already running.
- */
- const int ROOT_UNKNOWN = 0;
- const int ROOT_NONE = 1;
- const int ROOT_TOO_OLD = 2;
- const int ROOT_MULTIPLE = 3;
- const int ROOT_MAGISK = 4;
- const int ROOT_KERNELSU = 5;
- const int ROOT_APATCH = 6;
-
- /** Nothing was flashed: no usable root implementation. Distinct from any installer exit code. */
- const int INSTALL_NO_ROOT = -1;
- /** The installer binary could not be started at all. */
- const int INSTALL_NOT_EXECUTED = -2;
- /** The zip named by the manager does not exist or is not readable by the daemon. */
- const int INSTALL_NO_SUCH_FILE = -3;
-
- ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) = 2;
-
- String[] enabledModules() = 3;
-
- boolean enableModule(String packageName) = 4;
-
- boolean disableModule(String packageName) = 5;
-
- boolean setModuleScope(String packageName, in List scope) = 6;
-
- List getModuleScope(String packageName) = 7;
-
- boolean isVerboseLog() = 11;
-
- void setVerboseLog(boolean enabled) = 12;
-
- ParcelFileDescriptor getVerboseLog() = 16;
-
- ParcelFileDescriptor getModulesLog() = 17;
-
- /**
- * The rotated log parts the daemon still holds, oldest first, as bare file names.
- *
- * getVerboseLog()/getModulesLog() only ever hand over the part being written. The daemon keeps
- * ten, so on a device that has been logging for an hour most of the history was unreachable.
- */
- List getLogParts(boolean verbose) = 53;
-
- /** Opens one part by the name getLogParts() returned. Any other name is refused. */
- ParcelFileDescriptor getLogPart(boolean verbose, String name) = 54;
-
- long getXposedVersionCode() = 18;
-
- String getXposedVersionName() = 19;
-
- int getXposedApiVersion() = 20;
-
- boolean clearLogs(boolean verbose) = 21;
-
- PackageInfo getPackageInfo(String packageName, int flags, int uid) = 22;
-
- void forceStopPackage(String packageName, int userId) = 23;
-
- void reboot() = 24;
-
- boolean uninstallPackage(String packageName, int userId) = 25;
-
- boolean isSepolicyLoaded() = 26;
-
- List getUsers() = 27;
-
- int installExistingPackageAsUser(String packageName, int userId) = 28;
-
- boolean systemServerRequested() = 29;
-
- int startActivityAsUserWithFeature(in Intent intent, int userId) = 30;
-
- ParcelableListSlice queryIntentActivitiesAsUser(in Intent intent, int flags, int userId) = 31;
-
- boolean dex2oatFlagsLoaded() = 32;
-
- /**
- * Whether to force a launcher entry for apps that declare none.
- *
- * Android 10 and later synthesise one; `show_hidden_icon_apps_enabled` decides whether they
- * appear. The argument used to mean the opposite of the manager's own label, and the write
- * itself has been failing on Android 12 and later since the hidden method it used changed
- * shape. Both are fixed together, so the name states the direction: true shows the icons.
- */
- void setForcedLauncherIcons(boolean force) = 33;
-
- void getLogs(in ParcelFileDescriptor zipFd) = 34;
-
- void restartFor(in Intent intent) = 35;
-
- boolean optimizePackage(String packageName) = 40;
-
- int getDex2OatWrapperCompatibility() = 44;
-
- boolean enableStatusNotification() = 47;
-
- void setEnableStatusNotification(boolean enable) = 48;
-
- boolean getIncludeNewApps(String packageName) = 51;
-
- boolean setIncludeNewApps(String packageName, boolean enable) = 52;
-
- /** One of the ROOT_* constants. Detected once and cached, as the detection shells out. */
- int getRootImplementation() = 55;
-
- /** What the root implementation calls itself, for the manager to quote. Null when unknown. */
- String getRootImplementationVersion() = 56;
-
- /**
- * Flashes a module zip through whatever root implementation is managing the device.
- *
- * The daemon already runs as root, so this execs the installer directly rather than going
- * through `su` — the same commands the project's own gradle install tasks use. Output is
- * streamed to [callback] *and* written to the daemon's log, so a flash that failed on a device
- * that is now unbootable can still be read out of a saved bug report.
- *
- * Returns immediately; the work runs on a daemon thread and reports through [callback].
- */
- void installFrameworkZip(String zipPath, IFrameworkInstallCallback callback) = 57;
-
- /**
- * Which build this daemon is, or null when it was not recorded.
- *
- * The version code is the commit count on origin/master, so a branch build and the official
- * build of the same count are indistinguishable by number alone. This is what tells them apart.
- *
- * Not a bare hash, despite the name: it is the build stamp, which names where the build came
- * from as well as what commit it was made from — `93d66473-JingMatrix-Vector` from CI,
- * `93d66473` from a clean local tree, `93d66473+thinkpad` from a modified one. The commit
- * always leads, so a caller that wants it takes the head and not the whole string; `-` is
- * followed by the repository that holds that commit, `+` by the machine holding changes that
- * no repository does.
- */
- String getFrameworkCommit() = 58;
-
- /** The module loads, as far as the framework is concerned. */
- const int MODULE_LOAD_OK = 0;
-
- /** Installed and enabled, but no APK path could be resolved for it. */
- const int MODULE_LOAD_NO_APK = 1;
-
- /**
- * Installed and enabled, and the framework still would not load it.
- *
- * Deliberately not more specific. The loader refuses a zip that will not parse, an APK with no
- * init files and one with no module classes in the same breath, and naming any single one of
- * those would be a guess.
- */
- const int MODULE_LOAD_UNUSABLE = 2;
-
- /**
- * Built against libxposed API 100, which this framework no longer loads.
- *
- * The one refusal the loader can name, and the one the reader can act on: the module is not
- * broken, it is old, and only its author can move it forward. It used to arrive as
- * MODULE_LOAD_UNUSABLE, which reads as "your module is broken".
- */
- const int MODULE_LOAD_UNSUPPORTED_API = 3;
-
- /**
- * Modules that are enabled and installed, and that the framework still cannot load.
- *
- * The daemon holds two notions of a module: the configuration, which is what the user asked
- * for, and the realisation — the resolved APK and parsed DEX it hands to a forking process.
- * They can legitimately disagree, and the difference used to be thrown away: such a module
- * simply appeared to be off, having switched itself off for reasons nobody could see. This is
- * that difference, so the manager can say what happened.
- */
- String[] getUnloadableModules() = 59;
-
- /** Why [getUnloadableModules] lists this one; MODULE_LOAD_OK when it does not. */
- int getModuleLoadState(String packageName) = 60;
-
- /** The current state of [setForcedLauncherIcons]; true is the platform default. */
- boolean forcedLauncherIcons() = 61;
-
- /**
- * Restarts the framework without rebooting the device — the "soft reboot".
- *
- * The only way to stop and start the system framework, which is what "force stop" would mean
- * for it. Every app on screen goes with it.
- */
- void softReboot() = 62;
-
- /**
- * The manager APK the module was flashed with, opened read-only, or null when it cannot be.
- *
- * For installing the manager as an ordinary app. The manager cannot read this file itself:
- * parasitically it runs as the host, whose UID has no business in the module directory, and
- * standalone it is the very thing being replaced. The daemon verifies the signature before
- * handing the descriptor over, so what comes back is the APK this framework would accept as its
- * own manager and not whatever happens to sit at that path.
- */
- ParcelFileDescriptor getManagerApk() = 63;
-}
diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/models/Application.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/models/Application.aidl
deleted file mode 100644
index 272f4c5a5..000000000
--- a/services/manager-service/src/main/aidl/org/lsposed/lspd/models/Application.aidl
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.lsposed.lspd.models;
-
-parcelable Application {
- String packageName;
- int userId;
-}
diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/models/UserInfo.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/models/UserInfo.aidl
deleted file mode 100644
index 382e502cd..000000000
--- a/services/manager-service/src/main/aidl/org/lsposed/lspd/models/UserInfo.aidl
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.lsposed.lspd.models;
-
-parcelable UserInfo {
- int id;
- String name;
-}
diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/DeviceUser.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/DeviceUser.aidl
new file mode 100644
index 000000000..ea88931c0
--- /dev/null
+++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/DeviceUser.aidl
@@ -0,0 +1,33 @@
+package org.matrix.vector.ipc;
+
+/**
+ * One Android user or profile on this device, reduced to what the manager displays.
+ *
+ * Named to say which side of the boundary it is on. It was called {@code UserInfo}, which is
+ * also the name of the platform's hidden {@code android.content.pm.UserInfo} that the daemon
+ * converts from - the conversion has both types in scope at once, told apart by nothing but
+ * an import line, and the manager had to write this one out fully qualified wherever it appeared.
+ *
+ *
+ * Two fields, deliberately. The platform type also carries flags, a creation time, a profile
+ * group and an icon path, none of which the manager reads and all of which would then have to be
+ * kept in step with whatever the platform does to them next.
+ */
+parcelable DeviceUser {
+ /**
+ * The user id, as everything about scope and package visibility is keyed on.
+ *
+ * 0 is the device owner. Not contiguous and not bounded by the number of users: profiles get
+ * their own ids, and a device that has had one removed leaves a gap.
+ */
+ int id;
+
+ /**
+ * What the platform calls this user, shown as-is.
+ *
+ * Whatever the user or the manufacturer named it, so it is display text and nothing may be
+ * parsed out of it. Neither unique nor stable - two profiles may carry one name, and a user can
+ * be renamed. {@link #id} is the identity.
+ */
+ String name;
+}
diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IFrameworkInstallReceiver.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IFrameworkInstallReceiver.aidl
new file mode 100644
index 000000000..05f0a4a05
--- /dev/null
+++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IFrameworkInstallReceiver.aidl
@@ -0,0 +1,58 @@
+package org.matrix.vector.ipc;
+
+/**
+ * Where a framework flash reports to, one line at a time and then once at the end.
+ *
+ * Implemented by the manager and handed to {@code IManagerService.installFrameworkZip}, which is
+ * the only thing that ever calls it - so it runs in the manager's process, on a daemon thread's
+ * initiative. It carries a binder descriptor of its own, but one that can only ever arrive through
+ * {@code IManagerService}, so a descriptor that matched there matches here.
+ *
+ * oneway throughout, and must stay so. The daemon must never block on the manager while
+ * an installer is running. The manager is a UI process that can be paused, killed or simply slow,
+ * and a flash that stalled because nobody read a line would be a flash abandoned halfway through -
+ * with the module tree in whatever state the installer had reached. A receiver that has gone away
+ * is logged and the flash continues, for the same reason.
+ */
+oneway interface IFrameworkInstallReceiver {
+ /**
+ * Nothing was flashed: no usable root implementation.
+ *
+ * The three sentinels are negative so they cannot be confused with what they share a channel
+ * with - a process exit status is 0 to 255, so nothing real ever lands here. They live on this
+ * interface rather than on the one that starts the flash because this is the only place they
+ * are ever delivered: {@code installFrameworkZip} answers with nothing and never returns
+ * one.
+ */
+ const int INSTALL_NO_ROOT = -1;
+
+ /** The installer binary could not be started at all. */
+ const int INSTALL_NOT_EXECUTED = -2;
+
+ /** The zip named by the manager does not exist, or the daemon cannot read it. */
+ const int INSTALL_NO_SUCH_FILE = -3;
+
+ /**
+ * One line of the installer's output, without its trailing newline.
+ *
+ * stdout and stderr merged, because an installer sends its diagnostics to one and its
+ * progress to the other, and reading them separately would interleave them in an order that is
+ * not the order they happened in. When an installer is actually started the first line is the
+ * command being run; the paths that refuse before that send a diagnostic instead, followed by
+ * the matching {@code INSTALL_*} code.
+ *
+ * Also written to the daemon's own log as it is sent, so a flash is readable afterwards out
+ * of a saved bug report even when nothing was watching at the time.
+ */
+ void onLine(String line);
+
+ /**
+ * The flash is over, and this is the last thing that will be said.
+ *
+ * @param exitCode the installer process's own status, where 0 is success - or one of the
+ * {@code INSTALL_*} values above, when there was no process to have a status. A
+ * reader that does not recognise a value must assume it is an exit status and
+ * show the number, rather than treat it as a failure it can name
+ */
+ void onFinished(int exitCode);
+}
diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl
new file mode 100644
index 000000000..19319ba12
--- /dev/null
+++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl
@@ -0,0 +1,762 @@
+package org.matrix.vector.ipc;
+
+import rikka.parcelablelist.ParcelableListSlice;
+
+import org.matrix.vector.ipc.DeviceUser;
+import org.matrix.vector.ipc.IFrameworkInstallReceiver;
+import org.matrix.vector.ipc.ModuleLoadFailure;
+import org.matrix.vector.ipc.ScopeEntry;
+
+/**
+ * What the manager app asks the daemon for, once the framework has pushed it a binder.
+ *
+ * Runs in the daemon, as root. Everything here is here because the manager cannot do it for
+ * itself: it is either the framework's own configuration and state, which nothing else holds, or a
+ * call into a system service that an ordinary app is not allowed to make.
+ *
+ * Authenticated once, by possession. This binder is registered nowhere.
+ * {@code IFrameworkService.requestManagerService} answers with it only for the pid the daemon
+ * launched the manager into, or for the uid of the installed manager package, and the injected
+ * framework then pushes it into that process by reflection. No method below re-checks its caller,
+ * and none needs to - the decision was taken when the binder was handed over. The consequence is
+ * that a process holding this binder holds the daemon's authority over the whole device, so it must
+ * never be published to servicemanager and never passed on.
+ *
+ * The fully qualified name of this interface is its binder descriptor, and the two ends can
+ * be different builds. {@link #getManagerApk} exists so the manager can be installed as an
+ * ordinary app, and an installed copy survives every later flash of the framework. Nothing about
+ * that failure is loud: the generated {@code Stub.asInterface} wraps any binder in a proxy without
+ * checking anything, the binder stays alive so {@code isBinderAlive()} keeps answering true, and
+ * every transaction then throws {@code SecurityException} out of {@code Parcel.enforceInterface}
+ * before its transaction code is even read - so the manager draws a framework that is plainly
+ * running as one that answers nothing, on every screen, with nothing said. The manager therefore
+ * compares {@code IBinder.getInterfaceDescriptor()} against its own compiled {@code DESCRIPTOR} the
+ * moment the binder arrives and before any transaction. That one question is exempt by
+ * construction - {@code INTERFACE_TRANSACTION} sits outside
+ * {@code FIRST_CALL_TRANSACTION..LAST_CALL_TRANSACTION}, which is the range the generated
+ * dispatcher checks the interface token for - so it is answered across any mismatch, and it names
+ * the build on the other end.
+ *
+ * Transaction ids are implicit, and {@link #getProtocolVersion} is what makes that safe.
+ * They are assigned in declaration order, so adding, removing or reordering a method shifts every
+ * id below it - and unlike the descriptor, nothing about that shift is visible to a peer built
+ * against a different revision. It would simply call a different method than it meant to. That is
+ * what numbering the methods by hand used to guard against, at the price of a number beside every
+ * one of them and a hole beside every one retired.
+ *
+ * A version handshake guards the same thing better. {@link #getProtocolVersion} is declared
+ * first, so it is transaction zero whatever else changes, and it is the first call the manager
+ * makes. A peer that disagrees is refused outright rather than left to call methods whose meaning
+ * has moved under it - which is what the numbers permitted: id 33 of the interface this replaces
+ * carried {@code setHiddenIcon(boolean hide)} and then {@code setForcedLauncherIcons(boolean force)},
+ * the same number with the argument's sense inverted, and every old peer went on calling it and
+ * asking for the opposite of what it meant.
+ *
+ * So the rule is not "append only". It is: change this file however the design wants, and bump
+ * {@link #PROTOCOL_VERSION} in the same commit.
+ *
+ * A {@code boolean} returned by a write means the daemon stored it, not the call
+ * arrived. Each such method says what a {@code false} means; ignoring it turns a refusal into a
+ * silent success. None of them means "the value was already that": writing a value a row already
+ * holds still answers true.
+ */
+interface IManagerService {
+
+ // ---- what this file is ---------------------------------------------------------------------
+
+ /**
+ * The generation of this interface a build was compiled from. Bump it whenever the method list
+ * changes in any way - added, removed, reordered, or a signature altered.
+ *
+ * Compiled into the manager as well as the daemon, so each side carries the number of the
+ * source it was built from, and {@link #getProtocolVersion} is how one asks the other. Since
+ * transaction ids follow declaration order, this number is the only thing standing between a
+ * mismatched pair and a call that lands on the wrong method.
+ */
+ const int PROTOCOL_VERSION = 1;
+
+ /**
+ * Which generation of this interface the daemon implements, never below 1.
+ *
+ * Answers the question the descriptor cannot. A matching descriptor means the two ends agree
+ * on what every id means; it does not mean the daemon has every id, and a call to
+ * a transaction the daemon does not implement is not an error - the driver answers
+ * {@code UNKNOWN_TRANSACTION}, {@code transact()} returns false, and the generated proxy then
+ * reads its result out of a reply parcel nothing wrote to. A missing {@code int} therefore
+ * arrives as 0, a missing {@code boolean} as false and a missing object as null, none of them
+ * distinguishable from a real answer. That silence is what {@link #ROOT_UNKNOWN} was given the
+ * value 0 to survive, one method at a time; this replaces the guessing for every method
+ * appended from here on.
+ *
+ * Must stay the first method declared. That is what pins it to transaction zero while
+ * everything below it is free to move, and it is the whole mechanism: a peer whose method list
+ * differs still agrees on where to ask what generation it speaks.
+ *
+ * It needs no fallback of its own, which is the one thing a version handshake usually cannot
+ * arrange: this descriptor is new, so every daemon answering to it was built from a file that
+ * already carries this method. A daemon too old to implement it answers 0 from an untouched
+ * reply parcel, and 0 is below the floor, so it is refused for the right reason anyway.
+ */
+ int getProtocolVersion();
+
+ // ---- what this framework is -----------------------------------------------------------------
+
+ /**
+ * This daemon's version code, which is the commit count on origin/master.
+ *
+ * A branch build and the official build at the same depth therefore wear the same number;
+ * {@link #getBuildStamp} is what tells them apart.
+ */
+ long getFrameworkVersionCode();
+
+ /** This daemon's version name. */
+ String getFrameworkVersionName();
+
+ /**
+ * The build stamp, or null when this build recorded none.
+ *
+ * Names where the build came from as well as what commit it was made from -
+ * {@code 93d66473-JingMatrix-Vector} from CI, {@code 93d66473} from a clean local tree,
+ * {@code 93d66473+thinkpad} from a modified one. The commit always leads, so a caller that
+ * wants only that takes the head and not the whole string; {@code -} is followed by the
+ * repository holding that commit, {@code +} by the machine holding changes that no repository
+ * does.
+ *
+ * Was called {@code getFrameworkCommit}, and its own documentation had to open by saying it
+ * was not a commit.
+ */
+ @nullable String getBuildStamp();
+
+ /**
+ * The libxposed API level this framework implements, verbatim from
+ * {@code IXposedService.LIB_API}.
+ *
+ * The one version number here that is not this framework's own, which is why it says
+ * libxposed and the three above say framework. The contrast is the point: a reader who sees the
+ * word once in the identity block knows which of the four is not about this build.
+ */
+ int getLibxposedApiVersion();
+
+ // ---- whether the framework is actually working ----------------------------------------------
+
+ /**
+ * Whether system_server reached the daemon and was given its framework service.
+ *
+ * Latched once system_server has identified itself to the bootstrap bridge as uid 1000,
+ * process {@code system}, with a life token, and its registration has succeeded - and
+ * never cleared afterwards, because the framework is in that process for as long as it lives.
+ * Both halves matter: registration fails when the life token cannot be linked to death, and a
+ * true here that only meant "it asked" would claim the framework is in system_server while no
+ * module hooking the system loads.
+ *
+ * False is unambiguous: the framework is not in system_server, and nothing hooking the
+ * system will run. It is also the answer for the whole of boot before system_server gets
+ * there, so a false read early is not yet evidence of a fault.
+ */
+ boolean isSystemServerAttached();
+
+ /**
+ * Whether the framework's SELinux policy is in force.
+ *
+ * One {@code checkSELinuxAccess}: may {@code u:r:dex2oat:s0} execute
+ * {@code u:object_r:dex2oat_exec:s0} without transitioning. That is the first line of the
+ * module's own {@code sepolicy.rule} and is not allowed by stock policy, so it is used as a
+ * canary for the whole file - the daemon does not enumerate its rules, it asks whether the
+ * first one took. A false therefore means the root implementation did not apply the file, and
+ * the rest of what it grants is missing too, rather than meaning this one rule is absent.
+ */
+ boolean isSepolicyLoaded();
+
+ /**
+ * One of the {@code DEX2OAT_*} constants: what the dex2oat wrapper is doing.
+ *
+ * A state, not a yes or no, which is why it is no longer called a compatibility. Maintained
+ * by an observer on {@code /sys/fs/selinux/enforce} that mounts and unmounts the wrapper as the
+ * device's SELinux state moves, so it changes without anyone asking.
+ *
+ * Read together with {@link #isDex2OatInliningDisabled}: they are two routes to one end, not
+ * two independent facts.
+ */
+ int getDex2OatWrapperState();
+
+ /**
+ * Whether {@code dalvik.vm.dex2oat-flags} carries {@code --inline-max-code-units=0}.
+ *
+ * The fallback route, and the reason it is asked at all. The framework needs the platform's
+ * dex2oat not to inline across the methods a module may hook. It gets that from the wrapper
+ * while the wrapper is mounted; when the wrapper is taken down the daemon sets this property
+ * instead, and deletes it again when the wrapper comes back. So this being true while
+ * {@link #getDex2OatWrapperState} is not {@link #DEX2OAT_OK} is the healthy fallback, and both
+ * being unhealthy at once is the only case worth reporting - neither on its own is.
+ */
+ boolean isDex2OatInliningDisabled();
+
+ /**
+ * The dex2oat wrapper is mounted and serving.
+ *
+ * Also the answer below Android 10, where there is no wrapper at all: the daemon only starts
+ * the machinery that would report on one from Android 10, and answers with this literal before
+ * then. "Working" and "not applicable on this release" are therefore the same value, so a
+ * caller must not render it as a supported feature without checking the release first - the
+ * manager drops the row entirely below Android 10 rather than claim anything about it.
+ */
+ const int DEX2OAT_OK = 0;
+
+ /**
+ * The daemon's own socket server for the wrapper died, and the wrapper was unmounted.
+ *
+ * The wrapper is a small binary bind-mounted over the platform's dex2oat, which asks the
+ * daemon over a unix socket for a descriptor to the real one. When that server throws, the
+ * mounts are taken down and this is latched: the SELinux observer that would otherwise re-mount
+ * them stops watching, so this state never recovers while the daemon lives.
+ */
+ const int DEX2OAT_CRASHED = 1;
+
+ /**
+ * The bind mounts over the platform's dex2oat binaries could not be established, or did not
+ * survive being checked. Latched, and for the same reason as {@link #DEX2OAT_CRASHED}: the
+ * observer stops watching, so this does not recover by itself either.
+ */
+ const int DEX2OAT_MOUNT_FAILED = 2;
+
+ /**
+ * SELinux is permissive, so the wrapper was unmounted.
+ *
+ * Not a failure and not latched - the daemon keeps watching
+ * {@code /sys/fs/selinux/enforce} and re-mounts when the device goes back to enforcing.
+ */
+ const int DEX2OAT_SELINUX_PERMISSIVE = 3;
+
+ /**
+ * An untrusted app can reach {@code dex2oat_exec}, so the wrapper was unmounted.
+ *
+ * The probe is two {@code checkSELinuxAccess} calls asking whether
+ * {@code u:r:untrusted_app:s0} may {@code execute} or {@code execute_no_trans}
+ * {@code u:object_r:dex2oat_exec:s0}. That it may is evidence the policy on this device is more
+ * permissive than the one the wrapper assumes, and hooking dex2oat under it would expose the
+ * wrapper to every app on the device. Re-checked on every SELinux event, so this recovers by
+ * itself.
+ */
+ const int DEX2OAT_SEPOLICY_INCORRECT = 4;
+
+ // ---- module configuration ---------------------------------------------------------------------
+
+ /**
+ * The enabled modules, by package name.
+ *
+ * Straight from the database and deliberately not from the module cache, which is rebuilt
+ * asynchronously: a caller that enables a module and reads back immediately - which the manager
+ * does, to confirm its own write - was told the state from before its own write, and then wrote
+ * that back over what it had correctly recorded. The row sat in the wrong section until the app
+ * restarted, by which time the cache had caught up and nothing looked wrong.
+ *
+ * The framework keeps a pseudo-module row of its own in the same table, so that its own
+ * settings have a foreign key to hang from. It can never be enabled, so it can never appear
+ * here.
+ */
+ List getEnabledModules();
+
+ /**
+ * Switches a module on or off.
+ *
+ * Enabling inserts the row when the package has never been seen, with an empty APK path for
+ * the next cache rebuild to fill in, and takes down the shade's "not activated yet" notice for
+ * that package - which nothing else was ever going to do. Disabling only updates, and takes
+ * nothing down. Both ask the daemon to rebuild its module cache, so the effect on a running
+ * process arrives later and only when that process next starts.
+ *
+ * @return whether the daemon stored it, which is not whether the call arrived. False means the
+ * package is the framework's own pseudo-module, or - when disabling - that no module row
+ * exists for it. Enabling a module that was already enabled still answers true
+ */
+ boolean setModuleEnabled(String packageName, boolean enabled);
+
+ /**
+ * A module's scope as configured, or null.
+ *
+ * Null only for the framework's own pseudo-module row, which is not a module and has no
+ * scope; every other package answers with a list, empty when nothing is scoped to it. Null and
+ * empty are different answers and a caller must keep them apart - treating a refusal as no rows
+ * turns an unreadable scope into an erased one on the next write.
+ *
+ * What comes back is the configuration, not what the framework will actually inject. A
+ * legacy module is additionally put into its own scope at cache-rebuild time - it reports
+ * itself active by hooking a method in its own app, so it has to be there - and that row is
+ * derived rather than stored, so it is not here and must not be written back as though it were.
+ * {@link #getIncludeNewApps} is the opposite case and needs no allowance: it widens a scope by
+ * writing ordinary rows through {@link #setModuleScope}, so they are here, and switching it off
+ * does not take them away again.
+ */
+ @nullable List getModuleScope(String packageName);
+
+ /**
+ * Replaces a module's scope with exactly this set.
+ *
+ * Not a merge: what is not in the list is removed. A caller that wants to add one entry must
+ * read the current set first, and must expect it to have changed since it last looked - a
+ * module can request scope for itself, and the daemon adds newly installed apps to a module
+ * that asked for that.
+ *
+ * This also switches the module on. A scope is meaningless on a module that is off,
+ * and every route into this call - the manager, the socket CLI, a backup restore, a module's
+ * own request - would otherwise have to remember to enable separately. The consequence to
+ * account for is that a caller with its own idea of the enabled state has to re-read it
+ * afterwards, or it will show a module that is off while its scope screen shows a scope that is
+ * live.
+ *
+ * @return false when the daemon refused or could not write: a module that fixes its own scope
+ * in its APK will not take a target outside it, and a database failure rolls the whole
+ * transaction back. A refusal is not a failed transaction, so a caller that only checks
+ * whether the call succeeded will show a scope the framework never took
+ */
+ boolean setModuleScope(String packageName, in List scope);
+
+ /**
+ * Whether a module is given each newly installed app automatically.
+ *
+ * @return false for a package the daemon holds no module row for, and for the framework's own
+ * pseudo-module - so a false here is not evidence that a module exists
+ */
+ boolean getIncludeNewApps(String packageName);
+
+ /**
+ * Sets that flag.
+ *
+ * @return whether the daemon stored it, which is not whether the call arrived: no row is
+ * written for a package that is not a known module, or for the framework's own
+ * pseudo-module. Writing the value the row already held still answers true
+ */
+ boolean setIncludeNewApps(String packageName, boolean enable);
+
+ /**
+ * Every module that is installed and switched on and that the framework still cannot load, with
+ * the reason for each.
+ *
+ * One call for the whole set. This replaces a pair - a list of names, then one transaction
+ * per name to ask why - whose caller had to seed every entry with a placeholder reason before
+ * the second round could overwrite it, so a single dropped transaction left a module reported
+ * as missing its APK, a claim nothing had established.
+ *
+ * Read out of the module cache, not the database, so unlike {@link #getEnabledModules} this
+ * lags a write: a module switched on a moment ago is absent from this list until the rebuild
+ * that write asked for has finished, whatever that rebuild will conclude. Presenting the two as
+ * one snapshot tells a reader their module loaded and then contradicts it on the next
+ * refresh.
+ */
+ List getModuleLoadFailures();
+
+ /** Installed and enabled, but no APK path could be resolved for it. */
+ const int MODULE_LOAD_NO_APK = 1;
+
+ /**
+ * Installed and enabled, and the framework still would not load it.
+ *
+ * Deliberately not more specific. The loader refuses a zip that will not parse, an APK with
+ * no init files and one with no module classes in the same breath, and naming any single one of
+ * those would be a guess.
+ */
+ const int MODULE_LOAD_UNUSABLE = 2;
+
+ /**
+ * Built against libxposed API 100, which this framework no longer loads.
+ *
+ * The one refusal the loader can name, and the one a reader can act on: the module is not
+ * broken, it is old, and only its author can move it forward. It used to arrive as
+ * {@link #MODULE_LOAD_UNUSABLE}, which reads as "your module is broken".
+ */
+ const int MODULE_LOAD_UNSUPPORTED_API = 3;
+
+ // ---- the framework's own settings ------------------------------------------------------------
+
+ /**
+ * Whether the framework posts its status notification. True on a device where nobody has said
+ * otherwise.
+ *
+ * Worth more than it looks on a parasitic install, where the manager has no launcher entry
+ * and that notification can be the only way back into it.
+ *
+ * Was called {@code enableStatusNotification}, which reads as a command and was called as
+ * one: the socket CLI invokes it in the branch that handles reading settings.
+ */
+ boolean isStatusNotificationEnabled();
+
+ /**
+ * Sets that, and reconciles the shade with it in the same call - posting the notification if it
+ * was off and is now on, cancelling it if it was on and is now off - so the shade never
+ * disagrees with the switch.
+ */
+ void setStatusNotificationEnabled(boolean enabled);
+
+ /**
+ * Whether the daemon is capturing the verbose log. True on a device where nobody has said
+ * otherwise.
+ *
+ * The stored value, not the value or'd with the build type. It used to be the latter, which
+ * made the setting unwritable on a debug daemon: the manager could never read false, so its
+ * switch snapped back on every tap and had to be greyed out.
+ */
+ boolean isVerboseLogEnabled();
+
+ /**
+ * Sets that, and asks the daemon's log reader to start or stop capturing to match.
+ *
+ * The reader acts on a sentinel written into the log rather than on this call returning, so
+ * capture is not yet in step when this comes back. What is already written stays written; only
+ * what is captured from here on changes.
+ */
+ void setVerboseLogEnabled(boolean enabled);
+
+ // ---- logs -------------------------------------------------------------------------------------
+
+ /**
+ * The part of one of the two logs that is being written right now, read-only, or null when the
+ * daemon holds no descriptor for it.
+ *
+ * One method rather than the two it replaces, because every other call in this group already
+ * takes the same boolean and the single caller was choosing between them by hand.
+ *
+ * The two were not symmetric and still are not: only the modules stream asks the daemon's
+ * log reader to re-open a descriptor it has lost. Levelling that would change when a lost
+ * verbose descriptor is repaired, which is a decision about the log and not one this merge is
+ * entitled to take.
+ */
+ @nullable ParcelFileDescriptor getLiveLogPart(boolean verbose);
+
+ /**
+ * The parts of that log still on disk, oldest first, as bare file names.
+ *
+ * {@link #getLiveLogPart} only ever hands over the part being written, and the daemon keeps
+ * ten, so on a device that has been logging for an hour most of the history was unreachable.
+ * Listed from the log directory rather than from the reader's own record of what it has opened,
+ * so a part the reader never had in hand is still offered. The names carry an ISO-8601
+ * timestamp, which is why this order is chronological.
+ *
+ * This daemon run's parts only. A restart moves the whole log directory aside and starts an
+ * empty one, so the previous run's parts are reachable through {@link #writeBugReport} and
+ * nowhere else.
+ */
+ List getLogParts(boolean verbose);
+
+ /**
+ * Opens one part by a name {@link #getLogParts} returned, read-only.
+ *
+ * Any other name is refused. The name arrives from an unprivileged process and is used to
+ * build a path inside a directory only root can read, so it is checked against that listing
+ * rather than pattern-matched for {@code ..}: traversal and anything outside the log directory
+ * are ruled out by construction.
+ *
+ * @return null for a name that is not one of the current parts, which includes a part that
+ * rotated away between the two calls
+ */
+ @nullable ParcelFileDescriptor getLogPart(boolean verbose, String name);
+
+ /**
+ * Closes the part being written and opens a fresh one.
+ *
+ * Nothing is deleted and nothing is truncated. The closed part stays on disk under
+ * the ten-part limit, stays reachable through {@link #getLogParts} and {@link #getLogPart}, and
+ * still travels in {@link #writeBugReport}. This was called {@code clearLogs}, which is what a
+ * caller offering it to a user will say, and the moment the part list was added that inaccuracy
+ * became a visible contradiction: the user cleared the log and the cleared lines were still one
+ * tap away.
+ *
+ * Answers nothing, and used not to: it returned a boolean that was the constant true, which
+ * the manager read as a success signal, so a rotation that never happened was reported as one
+ * that had. There is nothing truthful to answer - the daemon asks its reader to rotate by
+ * writing a sentinel into the log and does not learn whether it acted. What did happen is
+ * visible in {@link #getLogParts}.
+ */
+ void startNewLogPart(boolean verbose);
+
+ /**
+ * Writes a bug report into {@code zipFd} as a zip.
+ *
+ * Far more than the logs, which is why it is no longer called {@code getLogs} and why the
+ * logs are the last thing added: tombstones and ANR traces, both crash directories, a full
+ * {@code logcat -b all -d} and {@code dmesg}, every root module's prop, remove, disable, update
+ * and sepolicy files, the {@code /proc} maps, mountinfo and status of the daemon and of the
+ * caller, the module database, and the resolved scopes rendered as text. The zip's comment
+ * names the build type, version, version code and build stamp, so an attached archive can be
+ * tied to a binary.
+ *
+ * Synchronous, and the slowest call here by a wide margin - it walks several directories and
+ * forks two commands before it deflates anything. Each side owns its copy of the descriptor and
+ * closes it.
+ *
+ * Errors met while filling the zip are logged and swallowed, so a partial archive arrives
+ * looking exactly like a complete one: this transaction succeeding is not evidence that
+ * everything is in it.
+ */
+ void writeBugReport(in ParcelFileDescriptor zipFd);
+
+ // ---- the device, as only a privileged process can see it ---------------------------------------
+
+ /**
+ * Every package installed for every real user on the device.
+ *
+ * The manager's own package manager sees one user, and the whole point of the app list is
+ * that a module may be scoped into another profile. Per user rather than merged: a device with
+ * a work profile or a private space holds the same package twice, under two uids, and a scope
+ * is chosen per copy. Each user is queried separately and an entry is kept only when its own
+ * uid belongs to the user it was listed for, because the platform will otherwise answer for
+ * packages that user does not hold.
+ *
+ * Also carries the clone users some Lenovo devices keep without reporting them in the user
+ * list, which have to be probed for by id.
+ *
+ * A {@code ParcelableListSlice} rather than a plain {@code List} because this is hundreds of
+ * {@code PackageInfo} objects on an ordinary device and does not fit in one binder transaction;
+ * the slice sends it in chunks, as AOSP's own hidden {@code ParceledListSlice} does for the
+ * same call.
+ *
+ * @param flags passed to the platform unchanged, widened to a long on Android 13 and
+ * later where the hidden method takes one
+ * @param filterNoProcess drops packages that declare no process, which can never be injected
+ * into and are only noise in a picker - at the cost of a second
+ * package-manager query per package
+ */
+ ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess);
+
+ /**
+ * Resolves an intent against one user's activities.
+ *
+ * Same reason as above: the manager cannot see another profile's activities at all, and the
+ * screen it needs is a module's own settings activity in whatever user holds it. The same slice
+ * wrapper, though this list is normally one entry.
+ */
+ ParcelableListSlice queryIntentActivitiesAsUser(in Intent intent, int flags, int userId);
+
+ /**
+ * The real users and profiles on this device, as id and name.
+ *
+ * Includes the ones a manufacturer hides, by the same probe {@link
+ * #getInstalledPackagesFromAllUsers} uses - a module can be installed in one, and would
+ * otherwise be invisible to the manager.
+ */
+ List getUsers();
+
+ // ---- things done to the device on the manager's behalf ------------------------------------------
+
+ /**
+ * Starts an activity as another user.
+ *
+ * Unless {@code noUserSwitch} is set, and unless the device is already on the target's
+ * profile parent, it is first switched to that parent and the screen is locked. That is the
+ * surprising part of this call. It is
+ * right for an activity that exists in one profile only, and a startling thing to do to someone
+ * who pressed "open" on a module whose window shows for whichever user is current anyway - so
+ * the caller decides, from the resolved activity's {@code FLAG_SHOW_FOR_ALL_USERS}.
+ *
+ * A parameter rather than the {@code lsp_no_switch_to_user} intent extra it replaces, and
+ * carrying that extra's sense unchanged so that neither side inverts a test while adopting it.
+ * The extra was a string agreed between two files in two APKs that can ship apart, which is a
+ * skew nothing else here is exposed to and one that fails quietly: a manager not updated in
+ * step spelled it differently, and the whole of the symptom was a device that changed user and
+ * locked itself when somebody opened a module.
+ *
+ * Returns the activity manager's own start code, so that a refusal reaches the caller
+ * instead of a flat success: a declined user switch, a disabled or unexported activity, an
+ * activity that has gone since it was resolved. A start succeeded when the code is 0 to 99,
+ * which is what {@code ActivityManager.isStartResultSuccessful} tests; those constants are
+ * hidden, so the band has to be written out. -100 to -1 is the fatal refusal band and 100 to
+ * 199 the non-fatal one, and nothing came up in either.
+ *
+ * Was called {@code startActivityAsUserWithFeature}, after the AOSP method of that name -
+ * whose distinguishing feature is a calling feature id that this signature does not have and
+ * the daemon never supplies.
+ */
+ int startActivityAsUser(in Intent intent, int userId, boolean noUserSwitch);
+
+ /**
+ * Force-stops a package for one user.
+ *
+ * Answers nothing, because the platform call answers nothing either: whether the transaction
+ * arrived is the only verdict there is, and a package that was not running and a refusal are
+ * the same silence.
+ */
+ void forceStopPackage(String packageName, int userId);
+
+ /**
+ * Uninstalls a package, as the {@code android} installer.
+ *
+ * {@link #ALL_USERS} for {@code userId} removes it from every user. That is what the manager
+ * needs to replace itself: a copy left behind in another profile refuses an install exactly as
+ * loudly as one in this profile.
+ *
+ * Blocks on a daemon binder thread until the package installer broadcasts its status, or for
+ * a minute, whichever comes first. The bound is what keeps a status that never arrives from
+ * holding that thread for the life of the daemon.
+ *
+ * @return whether the installer reported success. A device-policy refusal, a user that does not
+ * exist, and a status that never arrived all come back as a plain false, which no
+ * exception would show - so this being false is not by itself evidence the package is
+ * still installed, only that nothing confirmed it is gone
+ */
+ boolean uninstallPackage(String packageName, int userId);
+
+ /**
+ * Clears an app's ART profiles and forces a profile-guided recompile.
+ *
+ * What the manager offers after a module's scope changes: the app's compiled code can hold
+ * decisions taken before it was hooked, and only recompiling takes them back out.
+ *
+ * Both steps, in that order, because {@code speed-profile} only compiles methods recorded in
+ * the reference profile and recompiling without clearing re-bakes a profile captured before the
+ * module set changed. Clearing is best effort and its failure is not reported: a recompile
+ * against a stale profile still beats abandoning the action. From Android 14 this goes through
+ * the ART Service shell, falling back to the hidden binder calls; on older releases it uses
+ * those directly, and they no longer exist on Android 17.
+ *
+ * @return whether the recompile succeeded
+ */
+ boolean optimizePackage(String packageName);
+
+ /**
+ * Restarts the framework without rebooting the device - the "soft reboot".
+ *
+ * Restarts the primary zygote, which is what system_server is forked from, so the whole
+ * framework goes. This is what "force stop" would mean for the framework, and everything on
+ * screen dies with it: the caller is expected to have said so first. It is also the only way to
+ * make system_server pick up a scope change, because it reads its module list once, when it
+ * starts.
+ *
+ * Distinct from the daemon's own restart of the secondary zygote, which exists for 64/32
+ * devices and is not reachable from here.
+ */
+ void softReboot();
+
+ /** Reboots the device. */
+ void reboot();
+
+ /**
+ * Whether apps that declare no launcher entry are given one anyway.
+ *
+ * Android 10 and later synthesise an entry for an installed app that declares none, and the
+ * global setting {@code show_hidden_icon_apps_enabled} decides whether it appears. Here rather
+ * than with the framework's own settings because the value is not the framework's: it lives in
+ * Android's global settings, anything on the device can move it, and the daemon reads it back
+ * rather than remembering what it wrote.
+ *
+ * Unset reads as true, which is the platform's own default - reading unset as "off" showed
+ * the opposite of what the system was doing on every device where nobody had touched it. True
+ * is also the answer when the read itself failed.
+ */
+ boolean isForcedLauncherIcons();
+
+ /**
+ * Sets that. True shows the icons.
+ *
+ * Answers nothing, and the write can fail without saying so: the daemon applies it by
+ * running the {@code settings} command rather than going through a binder. That is neither
+ * laziness nor a shortcut - two in-process routes were tried and both are closed to this
+ * process, because the pre-Android-12 {@code IContentProvider.call} signature no longer exists
+ * and going through the system context's content resolver fails at the far end, where the
+ * daemon has an {@code ActivityThread} but no application record to be given a provider for. A
+ * caller that needs to know whether the setting moved must read {@link #isForcedLauncherIcons}
+ * back.
+ *
+ * The argument used to mean the opposite, under the name {@code setHiddenIcon(boolean
+ * hide)}, at the same transaction id - see the note on this interface.
+ */
+ void setForcedLauncherIcons(boolean force);
+
+ /**
+ * The user id {@link #uninstallPackage} reads as "every user this package is installed for".
+ *
+ * The daemon's own convention rather than a platform one: it becomes
+ * {@code PackageManager.DELETE_ALL_USERS} and a target user of 0. Declared here because it is
+ * the only place it can be agreed - the manager had to define its own copy of this number, next
+ * to a comment repeating what the daemon does with it.
+ */
+ const int ALL_USERS = -1;
+
+ // ---- installing and updating the framework -------------------------------------------------------
+
+ /**
+ * Which root implementation is managing this device, as one of the {@code ROOT_*} constants.
+ *
+ * Presence only: whether the zygisk loader will run on this device is the loader's own
+ * decision, taken before the daemon exists, so a daemon that is answering has already passed
+ * it. Detected once and cached, because detecting it forks each candidate binary. A binary that
+ * exists but exits non-zero is not counted - a leftover {@code magisk} from a previous root
+ * manager answers "Cannot connect to daemon", and counting it would turn a working KernelSU
+ * device into {@link #ROOT_MULTIPLE}.
+ */
+ int getRootImplementation();
+
+ /**
+ * Flashes the framework's own root-module zip through whatever root implementation is managing
+ * the device.
+ *
+ * "Module" there means a Magisk, KernelSU or APatch module, which is what the framework
+ * ships as - not an Xposed module, which is what the word means everywhere else in this
+ * file.
+ *
+ * The daemon already runs as root, so this execs the implementation's own installer directly
+ * rather than going through {@code su} - the same commands the project's gradle install tasks
+ * use, so a zip that flashes from a developer's machine flashes the same way from the
+ * device.
+ *
+ * Returns as soon as the work has been handed to a daemon thread. "The daemon accepted it"
+ * and "the flash finished" are therefore two events on two channels, deliberately: a flash runs
+ * for minutes and can end in a reboot, and a caller suspended until this returned would be
+ * suspended across that. Output is streamed to {@code receiver} and written to the
+ * daemon's log, so a flash that failed on a device that no longer boots can still be read out
+ * of a saved bug report. A receiver that has gone away does not stop the flash: stopping
+ * halfway would leave the module tree half written.
+ *
+ * @param zipPath a path the daemon can read; one it cannot is reported as
+ * {@code IFrameworkInstallReceiver.INSTALL_NO_SUCH_FILE} rather than refused
+ * here
+ * @param receiver where the output and the exit status arrive
+ */
+ void installFrameworkZip(String zipPath, IFrameworkInstallReceiver receiver);
+
+ /**
+ * The manager APK this framework was flashed with, opened read-only, or null.
+ *
+ * For installing the manager as an ordinary app. The manager cannot read this file itself:
+ * parasitically it runs as its host process, whose uid has no business in the module directory,
+ * and standalone it is the very thing being replaced. The daemon verifies the signature before
+ * handing the descriptor over, so what comes back is the APK this framework would accept as its
+ * own manager and not whatever happens to sit at that path - the same file and the same check
+ * as {@code IFrameworkService.openManagerApk}, which serves it to a host process for
+ * injection.
+ *
+ * @return null for three cases that are deliberately not told apart, because none of them
+ * leaves anything to offer: the file is missing, its signature is not the one this
+ * framework accepts, or the daemon is too old to answer at all
+ */
+ @nullable ParcelFileDescriptor getManagerApk();
+
+ /**
+ * The daemon did not say which root implementation is installed.
+ *
+ * Takes 0 because 0 is also what a binder proxy hands back for a transaction the daemon does
+ * not implement. {@link #ROOT_NONE} used to sit here, so a daemon too old to answer read as "no
+ * root installed", and the manager told a rooted user to go and install the root manager they
+ * were already running. Never produced by the daemon itself.
+ */
+ const int ROOT_UNKNOWN = 0;
+
+ /** No root implementation was found, so nothing can be flashed. */
+ const int ROOT_NONE = 1;
+
+ /**
+ * More than one was found.
+ *
+ * Not a failure in any of them - a device with two root implementations installed, where
+ * flashing through either would be guessing which one owns the module tree on the reader's
+ * behalf.
+ */
+ const int ROOT_MULTIPLE = 2;
+
+ /** Magisk. */
+ const int ROOT_MAGISK = 3;
+
+ /** KernelSU. */
+ const int ROOT_KERNELSU = 4;
+
+ /** APatch. */
+ const int ROOT_APATCH = 5;
+}
diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/ModuleLoadFailure.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/ModuleLoadFailure.aidl
new file mode 100644
index 000000000..62c87f786
--- /dev/null
+++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/ModuleLoadFailure.aidl
@@ -0,0 +1,28 @@
+package org.matrix.vector.ipc;
+
+/**
+ * A module the user switched on that the framework could not load, and why.
+ *
+ * The gap between the two notions of a module the daemon holds: the configuration, which is what
+ * the user asked for, and the realisation - the resolved APK and parsed dex it hands to a forking
+ * process. The difference used to be thrown away, so such a module simply appeared to be off,
+ * having switched itself off for reasons nobody could see.
+ *
+ * Only failures are described, and absence from the list is the answer for every other module:
+ * it loaded, or it is switched off and there was nothing to load.
+ */
+parcelable ModuleLoadFailure {
+ /** The module app's package name, which is the module's identity everywhere. */
+ String packageName;
+
+ /**
+ * One of {@code IManagerService.MODULE_LOAD_NO_APK} and the values beside it.
+ *
+ * Never 0. 0 is what a reader would get out of an untouched reply parcel, so leaving it
+ * unclaimed keeps "the daemon did not answer" from arriving as a diagnosis. A reader that does
+ * not recognise a value must say the module could not be loaded rather than name the nearest
+ * reason it does know - naming one is what the pair this replaced forced its caller into, and
+ * it named the wrong one.
+ */
+ int reason;
+}
diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/ScopeEntry.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/ScopeEntry.aidl
new file mode 100644
index 000000000..6265ca3b5
--- /dev/null
+++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/ScopeEntry.aidl
@@ -0,0 +1,38 @@
+package org.matrix.vector.ipc;
+
+/**
+ * One line of a module's scope: an app the module is to be loaded into, and whose copy of it.
+ *
+ * Named for what it is. It was called {@code Application}, which says nothing about scope and
+ * collides with {@code android.app.Application} - both callers that handled a list of these had to
+ * write the type out fully qualified to say which one they meant, and both then mirrored it into a
+ * local class of the same shape under a name that did say.
+ *
+ * A structured parcelable, so it arrives as a bean with no equality of its own. A caller doing
+ * set arithmetic over scopes still needs a value type to do it with, and keeps one.
+ */
+parcelable ScopeEntry {
+ /**
+ * The app to load the module into.
+ *
+ * {@code system} is not a package but the system framework, and is the one target that
+ * belongs to no user.
+ */
+ String packageName;
+
+ /**
+ * Which installed copy of {@link #packageName} is meant.
+ *
+ * The target's user, not the module's: a module is one package, one APK and one scope
+ * set for the whole device, because Android cannot hold two different builds under one package
+ * name. The daemon refuses to expand a row whose user does not hold the module, which is what
+ * keeps a module installed for one user out of another user's processes.
+ *
+ * Stored as 0 whatever is written here when {@link #packageName} is the system framework:
+ * there is one system_server for the whole device, so a module in a work profile hooking the
+ * framework is hooking the same process as everyone else. Normalised rather than refused -
+ * refusing silently lost the one target a module may have cared about when a backup written by
+ * an older manager, which recorded the framework under the module's own user, was restored.
+ */
+ int userId;
+}
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt
index 94240a965..1acefd69f 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt
@@ -14,7 +14,7 @@ import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.util.concurrent.ConcurrentHashMap
import org.matrix.vector.ipc.IModuleService
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
import org.matrix.vector.impl.hooks.VectorCtorInvoker
import org.matrix.vector.impl.hooks.VectorHookBuilder
import org.matrix.vector.impl.hooks.VectorMethodInvoker
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt
index c72515192..d478cae00 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt
@@ -6,7 +6,7 @@ import androidx.annotation.RequiresApi
import io.github.libxposed.api.XposedModule
import io.github.libxposed.api.XposedModuleInterface.*
import java.util.concurrent.ConcurrentHashMap
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
/** Manages the dispatching of modern lifecycle events to loaded modules. */
object VectorLifecycleManager {
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt
index 77ee6c173..a29e9b355 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt
@@ -9,7 +9,7 @@ import java.util.TreeMap
import java.util.concurrent.ConcurrentHashMap
import org.matrix.vector.ipc.IModuleService
import org.matrix.vector.ipc.IRemotePreferenceCallback
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
@Suppress("DEPRECATION", "UNCHECKED_CAST")
private inline fun Bundle.getSerializableCompat(key: String): T? {
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt
index 4ca9f47a8..3b73fa34b 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt
@@ -1,7 +1,8 @@
package org.matrix.vector.impl.core
import java.lang.reflect.Executable
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Log
+import org.matrix.vector.util.Utils
import org.matrix.vector.nativebridge.HookBridge
/**
@@ -34,7 +35,7 @@ object VectorDeopter {
HookBridge.deoptimizeMethod(executable)
}
.onFailure {
- Utils.Log.v(
+ Log.v(
TAG,
"Skipping deopt for ${target.className}#${target.methodName}: ${it.message}",
)
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt
index 258e59ae3..9de0477cf 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt
@@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import org.matrix.vector.ipc.HotReloadOutcome
import org.matrix.vector.ipc.LoadedModule
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
import org.matrix.vector.impl.VectorContext
import org.matrix.vector.impl.VectorLifecycleManager
import org.matrix.vector.impl.hooks.VectorHookBuilder
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorProcessChannel.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorProcessChannel.kt
index 31b9146d2..deab0e888 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorProcessChannel.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorProcessChannel.kt
@@ -7,7 +7,7 @@ import java.util.concurrent.Executors
import org.matrix.vector.ipc.LoadedModule
import org.matrix.vector.ipc.IHotReloadOutcomeReceiver
import org.matrix.vector.ipc.IProcessChannel
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
private const val TAG = "VectorProcessChannel"
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt
index e59dd8d85..33710418e 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt
@@ -5,7 +5,7 @@ import android.os.ParcelFileDescriptor
import org.matrix.vector.ipc.LoadedModule
import org.matrix.vector.ipc.IProcessChannel
import org.matrix.vector.ipc.IFrameworkService
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
/**
* Singleton client for managing IPC communication with the injected manager service. Handles Binder
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt
index 01ef8d92c..3ef45d03b 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt
@@ -4,7 +4,7 @@ import android.app.ActivityThread
import android.os.Build
import android.os.IBinder
import dalvik.system.DexFile
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
import org.matrix.vector.ipc.IFrameworkService
import org.matrix.vector.impl.di.VectorBootstrap
import org.matrix.vector.impl.hookers.*
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt
index bf696175c..e8038befe 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt
@@ -1,7 +1,7 @@
package org.matrix.vector.impl.hookers
import io.github.libxposed.api.XposedInterface
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
/**
* Intercepts uncaught exceptions in the framework to provide diagnostic logging before the process
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt
index a95911755..210eea922 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt
@@ -6,7 +6,7 @@ import androidx.annotation.RequiresApi
import io.github.libxposed.api.XposedInterface
import java.util.Collections
import java.util.WeakHashMap
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
import org.matrix.vector.impl.VectorLifecycleManager
import org.matrix.vector.impl.di.LegacyPackageInfo
import org.matrix.vector.impl.di.VectorBootstrap
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt
index 9d0982bff..3cbfdd02f 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt
@@ -5,7 +5,7 @@ import io.github.libxposed.api.XposedInterface.ExceptionMode
import io.github.libxposed.api.XposedInterface.Hooker
import java.lang.reflect.Executable
import java.util.Collections
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
/**
* A registered hook configuration, stored natively by [HookBridge].
diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt
index 503fc49a8..fc0ffc5b7 100644
--- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt
+++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt
@@ -11,7 +11,7 @@ import java.lang.reflect.Executable
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
import org.matrix.vector.impl.di.VectorBootstrap
import org.matrix.vector.nativebridge.HookBridge
diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts
index dcfe1846c..a105a84c5 100644
--- a/zygisk/build.gradle.kts
+++ b/zygisk/build.gradle.kts
@@ -168,7 +168,7 @@ androidComponents {
)
into("framework") {
from(dexOutPath)
- rename("classes.dex", "lspd.dex")
+ rename("classes.dex", "vector.dex")
}
val injected = objects.newInstance(tempModuleDir.get().asFile.path)
doLast {
diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh
index 44707fee7..049ca7b8e 100644
--- a/zygisk/module/customize.sh
+++ b/zygisk/module/customize.sh
@@ -82,7 +82,7 @@ esac
ui_print "- Device platform: $ARCH ($ABI32 / $ABI64)"
ui_print "- Extracting root module files"
-for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex cli daemon.apk daemon manager.apk; do
+for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/vector.dex cli daemon.apk daemon manager.apk; do
extract "$ZIPFILE" "$file" "$MODPATH"
done
diff --git a/zygisk/module/daemon b/zygisk/module/daemon
index faca6adef..3b1dec4a5 100644
--- a/zygisk/module/daemon
+++ b/zygisk/module/daemon
@@ -43,4 +43,4 @@ fi
[ "$debug" = "true" ] && log -p d -t "Vector" "Starting daemon $*"
# Launch the daemon
-exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.matrix.vector.daemon.VectorDaemon "$@" >/dev/null 2>&1
+exec /system/bin/app_process $java_options /system/bin --nice-name=vectord org.matrix.vector.daemon.VectorDaemon "$@" >/dev/null 2>&1
diff --git a/zygisk/src/main/kotlin/org/matrix/vector/GrapheneDclHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/GrapheneDclHooker.kt
index 3873dd69c..28c38c754 100644
--- a/zygisk/src/main/kotlin/org/matrix/vector/GrapheneDclHooker.kt
+++ b/zygisk/src/main/kotlin/org/matrix/vector/GrapheneDclHooker.kt
@@ -4,7 +4,7 @@ import android.content.pm.ApplicationInfo
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
/**
* Exempts the parasitic manager's host package from GrapheneOS's "Restrict dynamic code loading"
diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt
index 79e4db7b2..8c6d16a5d 100644
--- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt
+++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt
@@ -18,15 +18,16 @@ import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import hidden.HiddenApiBridge
+import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
-import org.lsposed.lspd.ILSPManagerService
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.ipc.IManagerService
+import org.matrix.vector.util.Utils
import org.matrix.vector.impl.core.VectorServiceClient
-/** The "Parasite" logic. Injects the LSPosed Manager APK into a host process (shell). */
+/** The "Parasite" logic. Injects the manager APK into a host process (shell). */
@SuppressLint("StaticFieldLeak")
object ParasiticManagerHooker {
private const val CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"
@@ -65,7 +66,10 @@ object ParasiticManagerHooker {
// contexts.
// We copy the APK to the host's cache as a workaround.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
- val dstPath = "${appInfo.dataDir}/cache/lsposed.apk"
+ // The pre-rename name, removed so an upgraded host is not left carrying a
+ // stale copy of the manager in its cache forever.
+ runCatching { File("${appInfo.dataDir}/cache/lsposed.apk").delete() }
+ val dstPath = "${appInfo.dataDir}/cache/vector-manager.apk"
runCatching {
FileInputStream(sourcePath).use { input ->
FileOutputStream(dstPath).use { output ->
@@ -143,7 +147,7 @@ object ParasiticManagerHooker {
) as Boolean
if (!ok) throw RuntimeException("setBinder returned false")
}
- .onFailure { Utils.logW("Could not send binder to LSPosed Manager", it) }
+ .onFailure { Utils.logW("Could not send binder to the manager", it) }
}
/**
@@ -174,7 +178,7 @@ object ParasiticManagerHooker {
.onFailure { logE("Failed to evict the cached LoadedApk of $packageName", it) }
}
- private fun hookForManager(managerService: ILSPManagerService) {
+ private fun hookForManager(managerService: IManagerService) {
// Hook 1: Swap ApplicationInfo during host binding
XposedHelpers.findAndHookMethod(
ActivityThread::class.java,
@@ -479,7 +483,7 @@ object ParasiticManagerHooker {
// and there is no point opening the APK for it.
val managerBinder = VectorServiceClient.requestManagerService() ?: return false
VectorServiceClient.openManagerApk()!!.use { pfd ->
- val managerService = ILSPManagerService.Stub.asInterface(managerBinder)
+ val managerService = IManagerService.Stub.asInterface(managerBinder)
if (isParasitic) {
managerFd = pfd.detachFd()
diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt
index dd8422db8..52c82a3ea 100644
--- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt
+++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt
@@ -5,7 +5,7 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Build
import java.lang.reflect.Field
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Utils
import org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker
import org.matrix.vector.impl.hooks.VectorHookBuilder
import org.matrix.vector.service.BridgeService
diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt
index 1b245ee26..f41c2a666 100644
--- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt
+++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt
@@ -3,7 +3,8 @@ package org.matrix.vector.core
import android.os.IBinder
import android.os.Process
import org.matrix.vector.ipc.IFrameworkService
-import org.lsposed.lspd.util.Utils
+import org.matrix.vector.util.Log
+import org.matrix.vector.util.Utils
import org.matrix.vector.BuildConfig
import org.matrix.vector.GrapheneDclHooker
import org.matrix.vector.ParasiticManagerHooker
@@ -43,7 +44,7 @@ object Main {
Startup.initXposed(isSystem, niceName, appDir, appService)
// Configure logging levels from the service client
- runCatching { Utils.Log.muted = VectorServiceClient.isLogMuted }
+ runCatching { Log.muted = VectorServiceClient.isLogMuted }
.onFailure { t -> Utils.logE("Failed to configure logs from service", t) }
// Check if this process is the designated Vector Manager.
diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt
index 45dfcad1c..09c7bd7ac 100644
--- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt
+++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt
@@ -8,7 +8,7 @@ import android.os.Parcel
import hidden.HiddenApiBridge.Binder_allowBlocking
import hidden.HiddenApiBridge.Context_getActivityToken
import org.matrix.vector.ipc.IVectorDaemon
-import org.lsposed.lspd.util.Utils.Log
+import org.matrix.vector.util.Log
/**
* Manages manual Binder transactions for the Vector framework.