From d97d5b0c701408d81962dfabdfdfc1c9b2351015 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:39 +0200 Subject: [PATCH 01/13] Move the manager's IPC surface out of org.lsposed.lspd and say what it does The daemon-side AIDL was cleaned in 34144a8b5, which left this for later: the manager's interface, the parcelables it carries, and the -keep rule naming them. The names had drifted from the code by more than a namespace. getXposedVersionCode returned this framework's version, not Xposed's; getFrameworkCommit had to open its own documentation by saying it was not a commit; enableStatusNotification read as a command and the socket CLI called it as one, from the branch that *reads* settings; dex2oatFlagsLoaded asked whether a property carried a flag; startActivityAsUserWithFeature was named after an AOSP method whose distinguishing feature this signature does not have. Application and UserInfo both collided with platform types, so every caller handling a list of either wrote the type out fully qualified and then kept a local mirror under a name that did say what it was. Three methods had no callers at all - restartFor was a daemon-side no-op with a manager wrapper documenting it as vestigial - and two pairs asked one question twice. getUnloadableModules plus getModuleLoadState is the pair that mattered: the daemon holds a Map and the AIDL sent its keys, then took one transaction per key for the values. That forced the caller to seed every entry with MODULE_LOAD_NO_APK before the second round could overwrite it, so a dropped transaction told the user the framework could not find their module's APK - a claim nothing had established. It is one call returning the map. The lsp_no_switch_to_user intent extra is gone, and it is worth naming because it was a string agreed between two APKs that can ship apart. Getting it wrong switched the device's user and locked the screen when somebody opened a module. It is a parameter. The hand-written transaction ids go with it. They existed to keep a method's number stable across revisions, and they did that badly: id 33 of the old interface carried setHiddenIcon(boolean hide) and later setForcedLauncherIcons(boolean force), the same number with the argument's sense inverted, so every peer built against the earlier file went on calling it and asking for the opposite of what it meant. A number beside each method cannot catch that; a version can. getProtocolVersion is declared first, which makes it transaction zero in every revision, and the manager asks it before anything else. The rule is no longer "append only" but "change what the design wants, and bump PROTOCOL_VERSION in the same commit". That descriptor change is the cost. The manager can be installed as an ordinary app - getManagerApk exists for that - and an installed copy survives a later flash, so a manager and a daemon of different builds is a supported configuration rather than an accident. It used to degrade quietly, one method at a time, which is why ROOT_UNKNOWN has to be 0 and why getLogParts is defended with orEmpty(). A changed descriptor does not degrade: every transaction throws SecurityException while the binder stays alive, so a manager older than the framework it is flashed beside answers nothing on every screen. getProtocolVersion is added at id 1 for the same reason, and can only be added now: every daemon answering to this descriptor was built from a file that already carries it, so it needs no bootstrap sentinel. That is the usual reason a version handshake cannot be retrofitted to a live interface. Every method, constant, field and parcelable now carries what it is for, who may call it, what a false or a null means, and the failure symptom for constraints that cannot be inferred from the signature. Nothing asserts behaviour that was not read out of the implementation first. --- .../org/matrix/vector/daemon/VectorDaemon.kt | 4 +- .../org/matrix/vector/daemon/VectorService.kt | 26 +- .../matrix/vector/daemon/data/ConfigCache.kt | 21 +- .../matrix/vector/daemon/data/FileSystem.kt | 2 +- .../vector/daemon/data/ModuleDatabase.kt | 14 +- .../matrix/vector/daemon/env/Dex2OatServer.kt | 14 +- .../matrix/vector/daemon/ipc/CliHandler.kt | 20 +- .../vector/daemon/ipc/ManagerService.kt | 133 +-- .../vector/daemon/ipc/SystemServerService.kt | 8 +- .../vector/daemon/utils/RootImplementation.kt | 29 +- manager/README.md | 15 +- manager/proguard-rules.pro | 2 +- .../vector/manager/demo/DemoActivity.kt | 4 +- .../vector/manager/demo/DemoScenario.kt | 42 +- .../vector/manager/demo/FakeManagerService.kt | 148 ++-- .../org/matrix/vector/manager/Constants.kt | 4 +- .../vector/manager/data/log/CrashReport.kt | 3 + .../vector/manager/data/model/BuildStamp.kt | 2 +- .../data/repository/BackupRepository.kt | 4 +- .../data/repository/FrameworkInstaller.kt | 19 +- .../data/repository/ManagerInstaller.kt | 8 +- .../vector/manager/di/ServiceLocator.kt | 8 +- .../matrix/vector/manager/ipc/DaemonClient.kt | 166 ++-- .../ui/components/PackageActionMenu.kt | 15 +- .../ui/screens/canary/CanaryViewModel.kt | 4 +- .../manager/ui/screens/home/HomeViewModel.kt | 43 +- .../ui/screens/home/SystemStatusScreen.kt | 31 +- .../manager/ui/screens/logs/LogsViewModel.kt | 36 +- .../ui/screens/modules/ModulesScreen.kt | 16 +- .../ui/screens/modules/ModulesViewModel.kt | 40 +- .../ui/screens/modules/ScopeViewModel.kt | 13 +- .../ui/screens/report/TroubleshootScreen.kt | 2 +- .../screens/update/FrameworkUpdateScreen.kt | 10 +- .../update/FrameworkUpdateViewModel.kt | 26 +- services/daemon-service/build.gradle.kts | 2 +- services/manager-service/build.gradle.kts | 2 +- .../lspd/IFrameworkInstallCallback.aidl | 23 - .../org/lsposed/lspd/ILSPManagerService.aidl | 224 ----- .../org/lsposed/lspd/models/Application.aidl | 6 - .../org/lsposed/lspd/models/UserInfo.aidl | 6 - .../org/matrix/vector/ipc/DeviceUser.aidl | 33 + .../vector/ipc/IFrameworkInstallReceiver.aidl | 58 ++ .../matrix/vector/ipc/IManagerService.aidl | 789 ++++++++++++++++++ .../matrix/vector/ipc/ModuleLoadFailure.aidl | 28 + .../org/matrix/vector/ipc/ScopeEntry.aidl | 38 + zygisk/build.gradle.kts | 2 +- zygisk/module/customize.sh | 2 +- zygisk/module/daemon | 2 +- .../org/matrix/vector/GrapheneDclHooker.kt | 2 +- .../matrix/vector/ParasiticManagerHooker.kt | 16 +- .../vector/ParasiticManagerSystemHooker.kt | 2 +- .../kotlin/org/matrix/vector/core/Main.kt | 5 +- .../matrix/vector/service/BridgeService.kt | 2 +- 53 files changed, 1455 insertions(+), 719 deletions(-) delete mode 100644 services/manager-service/src/main/aidl/org/lsposed/lspd/IFrameworkInstallCallback.aidl delete mode 100644 services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl delete mode 100644 services/manager-service/src/main/aidl/org/lsposed/lspd/models/Application.aidl delete mode 100644 services/manager-service/src/main/aidl/org/lsposed/lspd/models/UserInfo.aidl create mode 100644 services/manager-service/src/main/aidl/org/matrix/vector/ipc/DeviceUser.aidl create mode 100644 services/manager-service/src/main/aidl/org/matrix/vector/ipc/IFrameworkInstallReceiver.aidl create mode 100644 services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl create mode 100644 services/manager-service/src/main/aidl/org/matrix/vector/ipc/ModuleLoadFailure.aidl create mode 100644 services/manager-service/src/main/aidl/org/matrix/vector/ipc/ScopeEntry.aidl 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..1c6611709 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) }) @@ -247,7 +247,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 +271,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/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 41b0a3beb..cfa5bdfc0 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,11 @@ 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 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,7 +47,7 @@ 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" @@ -217,13 +218,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 +236,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 +268,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 +282,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 +310,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,12 +372,13 @@ 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 @@ -378,27 +392,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 +428,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 +455,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 +478,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 +498,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) = @@ -505,22 +510,22 @@ object ManagerService : ILSPManagerService.Stub() { 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/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt index e961f4743..2b67084b7 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 @@ -76,9 +76,9 @@ object SystemServerService : Binder(), IBinder.DeathRecipient { 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 + // Return the FrameworkService singleton if successfully registered + return if (FrameworkService.registerHeartBeat(uid, pid, processName, processLifeToken)) { + FrameworkService } else null } @@ -107,7 +107,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..ec04f6020 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,7 +4,8 @@ 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" @@ -77,10 +78,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 } @@ -92,7 +93,7 @@ object RootImplementation { 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, + if (supported) IManagerService.ROOT_MAGISK else IManagerService.ROOT_TOO_OLD, "Magisk ${name ?: code}", binary, ) @@ -115,7 +116,7 @@ object RootImplementation { 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) } /** @@ -131,9 +132,9 @@ object RootImplementation { 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) + code == null -> Detection(IManagerService.ROOT_APATCH, "APatch ($output)", binary) + code >= MIN_APATCH -> Detection(IManagerService.ROOT_APATCH, "APatch $code", binary) + else -> Detection(IManagerService.ROOT_TOO_OLD, "APatch $code", binary) } } @@ -167,9 +168,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 +190,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 +199,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 +223,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/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..6f5e1cb8b 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,9 +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 libxposedApiVersion: Int = -1, + val frameworkVersionCode: Long = -1, + val rootImplementation: Int = IManagerService.ROOT_MAGISK, val rootVersion: String? = "28.1", val install: InstallScript = InstallScript.SUCCEEDS, @@ -115,23 +115,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,13 +149,13 @@ 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, + rootImplementation = IManagerService.ROOT_NONE, rootVersion = null, install = DemoScenario.InstallScript.NO_ROOT, ), @@ -163,7 +163,7 @@ val DEMO_SCENARIOS: List = 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, + rootImplementation = IManagerService.ROOT_MULTIPLE, rootVersion = null, install = DemoScenario.InstallScript.NO_ROOT, ), @@ -171,7 +171,7 @@ val DEMO_SCENARIOS: List = id = "root-too-old", title = "Root implementation too old", summary = "Installed but not usable. Distinct from having none.", - rootImplementation = ILSPManagerService.ROOT_TOO_OLD, + rootImplementation = IManagerService.ROOT_TOO_OLD, rootVersion = "20.4", install = DemoScenario.InstallScript.NO_ROOT, ), @@ -179,27 +179,27 @@ val DEMO_SCENARIOS: List = id = "root-ksu", title = "KernelSU", summary = "The install path quotes the implementation it found.", - rootImplementation = ILSPManagerService.ROOT_KERNELSU, + rootImplementation = IManagerService.ROOT_KERNELSU, rootVersion = "12045", ), DemoScenario( id = "root-apatch", title = "APatch", summary = "As above, third implementation.", - rootImplementation = ILSPManagerService.ROOT_APATCH, + rootImplementation = IManagerService.ROOT_APATCH, rootVersion = "10763", ), 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..14dde3051 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,36 +73,36 @@ 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 @@ -102,16 +113,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 +133,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 +141,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 +195,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 +242,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 +258,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 +277,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..790119959 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,7 @@ object Constants { @JvmStatic fun setBinder(binder: IBinder): Boolean { - ServiceLocator.bind(ILSPManagerService.Stub.asInterface(binder)) + ServiceLocator.bind(IManagerService.Stub.asInterface(binder)) 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..dc1ca9c7e 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,7 @@ 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() val context: Context get() = @@ -281,7 +281,7 @@ object ServiceLocator { } /** Called from `Constants.setBinder`, possibly before [attach]. */ - fun bind(service: ILSPManagerService?) { + fun bind(service: IManagerService?) { _service.value = service } } 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..a547b4f42 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> = runIpc { service -> + service.moduleLoadFailures.associate { it.packageName to it.reason } } suspend fun setModuleEnabled(packageName: String, enable: Boolean): Result = runIpc { - if (enable) { - it.enableModule(packageName) - } else { - it.disableModule(packageName) - } + it.setModuleEnabled(packageName, enable) } - suspend fun getFrameworkCommit(): Result = runIpc { it.frameworkCommit } + suspend fun getBuildStamp(): Result = runIpc { it.buildStamp } - suspend fun getXposedVersionName(): Result = runIpc { it.xposedVersionName } + suspend fun getFrameworkVersionName(): Result = runIpc { it.frameworkVersionName } - suspend fun getXposedVersionCode(): Result = runIpc { it.xposedVersionCode } + suspend fun getFrameworkVersionCode(): Result = runIpc { it.frameworkVersionCode } suspend fun getInstalledPackagesFromAllUsers( flags: Int, @@ -194,23 +185,37 @@ class DaemonClient(private val serviceState: StateFlow) { suspend fun setModuleScope( packageName: String, - applications: List, + applications: List, ): Result = runIpc { it.setModuleScope(packageName, applications) } + /** + * A module's configured scope. + * + * The AIDL answers null for the framework's own pseudo-module row, which is not a module and has + * no scope — and null is emphatically not an empty scope: reading a refusal as "no rows" and + * writing that back is how a scope gets erased. AIDL's Java backend emits no nullability + * annotations, so that null arrives as an unchecked platform type; it is turned into a failure + * here so the caller's existing error path takes it rather than a `List` that is null at + * runtime. + */ suspend fun getModuleScope( packageName: String - ): Result> = runIpc { it.getModuleScope(packageName) + ): Result> = runIpc { + it.getModuleScope(packageName) + ?: throw IllegalArgumentException("$packageName has no scope to read") } - suspend fun enableStatusNotification(): Result = runIpc { it.enableStatusNotification() + suspend fun isStatusNotificationEnabled(): Result = runIpc { + it.isStatusNotificationEnabled } - suspend fun setEnableStatusNotification(enabled: Boolean): Result = runIpc { it.setEnableStatusNotification(enabled) + suspend fun setStatusNotificationEnabled(enabled: Boolean): Result = runIpc { + it.setStatusNotificationEnabled(enabled) } - suspend fun isVerboseLogEnabled(): Result = runIpc { it.isVerboseLog } + suspend fun isVerboseLogEnabled(): Result = runIpc { it.isVerboseLogEnabled } - suspend fun setVerboseLogEnabled(enabled: Boolean): Result = runIpc { it.isVerboseLog = enabled + suspend fun setVerboseLogEnabled(enabled: Boolean): Result = runIpc { it.setVerboseLogEnabled(enabled) } /** @@ -235,18 +240,20 @@ class DaemonClient(private val serviceState: StateFlow) { * "the daemon is unreachable" and "there is no log file yet" are different situations, the Logs * screen renders them differently, and a `Result` would collapse them. */ - suspend fun getLog(verbose: Boolean): Result = runIpc { - if (verbose) it.verboseLog else it.modulesLog + suspend fun getLiveLogPart(verbose: Boolean): Result = runIpc { + it.getLiveLogPart(verbose) } - suspend fun clearLogs(verbose: Boolean): Result = runIpc { it.clearLogs(verbose) - } - - suspend fun getPackageInfo( - packageName: String, - flags: Int, - userId: Int, - ): Result = runIpc { it.getPackageInfo(packageName, flags, userId) + /** + * Closes the part being written and opens a fresh one. Nothing is deleted. + * + * `Result` because there is nothing truthful to answer: the daemon asks its log reader to + * rotate by writing a sentinel and never learns whether it acted. The call used to answer a + * constant `true`, which the Logs screen read as a success signal — so a rotation that never + * happened was reported as one that had. Success here means the daemon took the request. + */ + suspend fun startNewLogPart(verbose: Boolean): Result = runIpc { + it.startNewLogPart(verbose) } suspend fun forceStopPackage(packageName: String, userId: Int): Result = runIpc { it.forceStopPackage(packageName, userId) @@ -259,23 +266,16 @@ class DaemonClient(private val serviceState: StateFlow) { suspend fun isSepolicyLoaded(): Result = runIpc { it.isSepolicyLoaded } - suspend fun getUsers(): Result> = runIpc { it.users + suspend fun getUsers(): Result> = runIpc { it.users } - suspend fun installExistingPackageAsUser(packageName: String, userId: Int): Result = - runIpc { - val INSTALL_SUCCEEDED = 1 - it.installExistingPackageAsUser(packageName, userId) == INSTALL_SUCCEEDED - } + suspend fun isSystemServerAttached(): Result = runIpc { it.isSystemServerAttached } - suspend fun systemServerRequested(): Result = runIpc { it.systemServerRequested() + suspend fun isDex2OatInliningDisabled(): Result = runIpc { + it.isDex2OatInliningDisabled } - suspend fun dex2oatFlagsLoaded(): Result = runIpc { it.dex2oatFlagsLoaded() } - - suspend fun getDex2OatWrapperCompatibility(): Result = runIpc { - it.dex2OatWrapperCompatibility - } + suspend fun getDex2OatWrapperState(): Result = runIpc { it.dex2OatWrapperState } suspend fun optimizePackage(packageName: String): Result = runIpc { it.optimizePackage(packageName) @@ -287,26 +287,24 @@ class DaemonClient(private val serviceState: StateFlow) { * More than the logs: the daemon adds tombstones, ANR traces, both crash directories, a full * logcat and dmesg, the module database and the resolved scopes. */ - suspend fun writeLogsTo(zipFd: android.os.ParcelFileDescriptor): Result = runIpc { - it.getLogs(zipFd) + suspend fun writeBugReportTo(zipFd: android.os.ParcelFileDescriptor): Result = runIpc { + it.writeBugReport(zipFd) } - /** Kept for the AIDL's shape: the daemon implements it as a no-op, so this asks for nothing. */ - suspend fun restartFor(intent: android.content.Intent): Result = runIpc { - it.restartFor(intent) - } - - suspend fun startActivityAsUserWithFeature( - intent: android.content.Intent, - userId: Int, - ): Result = runIpc { it.startActivityAsUserWithFeature(intent, userId) } - - suspend fun queryIntentActivitiesAsUser( + /** + * Starts an activity as another user. + * + * [noUserSwitch] 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 resolved activity's `FLAG_SHOW_FOR_ALL_USERS` says which case this is. + */ + suspend fun startActivityAsUser( intent: android.content.Intent, - flags: Int, userId: Int, - ): Result> = runIpc { it.queryIntentActivitiesAsUser(intent, flags, userId).list - } + noUserSwitch: Boolean, + ): Result = runIpc { it.startActivityAsUser(intent, userId, noUserSwitch) } /** Restarts the framework without rebooting the device. Everything on screen goes with it. */ suspend fun softReboot(): Result = runIpc { it.softReboot() } @@ -329,7 +327,7 @@ class DaemonClient(private val serviceState: StateFlow) { * True is the platform default, and is what the daemon answers on a device where nothing has * ever set it. */ - suspend fun forcedLauncherIcons(): Result = runIpc { it.forcedLauncherIcons() } + suspend fun isForcedLauncherIcons(): Result = runIpc { it.isForcedLauncherIcons } suspend fun setForcedLauncherIcons(force: Boolean): Result = runIpc { it.setForcedLauncherIcons(force) @@ -358,14 +356,14 @@ class DaemonClient(private val serviceState: StateFlow) { /** * Starts a flash and returns as soon as the daemon has accepted it. * - * Deliberately not wrapped into a suspend-until-finished call: the result arrives on [callback] + * Deliberately not wrapped into a suspend-until-finished call: the result arrives on [receiver] * over minutes, and a coroutine suspended across a reboot-inducing operation is a coroutine - * that never resumes. The caller keeps the callback alive for as long as it wants the output. + * that never resumes. The caller keeps the receiver alive for as long as it wants the output. */ suspend fun installFrameworkZip( zipPath: String, - callback: IFrameworkInstallCallback, - ): Result = runIpc { it.installFrameworkZip(zipPath, callback) } + receiver: IFrameworkInstallReceiver, + ): Result = runIpc { it.installFrameworkZip(zipPath, receiver) } } /** diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt index d1e1f8365..b25b2ca01 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt @@ -267,11 +267,18 @@ LocalizedOverlay { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", packageName, null)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val started = daemon.startActivityAsUserWithFeature(intent, userId) - // The dominant failure is not an exception: the daemon returns a negative int for - // a null activity manager or a refused user switch. + // `noUserSwitch = false`, so the daemon switches to the target's profile parent + // and locks the screen first. That is right here: this is Settings' own details + // page for a package in that profile, which is not an activity that shows for + // whichever user is current. + val started = daemon.startActivityAsUser(intent, userId, noUserSwitch = false) + // The dominant failure is not an exception: the daemon hands back the activity + // manager's own start code, so a refused user switch or a screen that would not + // start arrives as a number rather than as a throw. Started means 0 to 99 — the + // band `ActivityManager.isStartResultSuccessful` tests, written out because those + // constants are hidden — with -100 to -1 fatal and 100 to 199 non-fatal refusals. val code = started.getOrDefault(-1) - if (code < 0) { + if (code !in 0..99) { logE( "actions: opening app info for $packageName as user $userId failed " + "(code $code)", diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryViewModel.kt index 101a60651..f51f8b0ea 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/canary/CanaryViewModel.kt @@ -62,13 +62,13 @@ class CanaryViewModel : ViewModel() { // reader who has come looking for a build that works. val installed = daemon - .getXposedVersionCode() + .getFrameworkVersionCode() .getOrElse { e -> logW("canary: framework version unavailable, using the manager's own", e) 0L } .takeIf { it > 0 } ?: BuildConfig.VERSION_CODE.toLong() - updates.refresh(installed, daemon.getFrameworkCommit().getOrNull()) + updates.refresh(installed, daemon.getBuildStamp().getOrNull()) attempted.value = true } viewModelScope.launch { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt index ffba7132e..90d3e27c7 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlin.random.Random import kotlinx.coroutines.launch -import org.lsposed.lspd.ILSPManagerService +import org.matrix.vector.ipc.IManagerService import org.matrix.vector.manager.data.github.CommunityFeed import org.matrix.vector.manager.data.github.GitHubRepository import org.matrix.vector.manager.data.model.ManagerCopy @@ -42,7 +42,7 @@ data class FrameworkStatus( val versionCode: Long = 0, val apiVersion: Int? = null, val issues: List = emptyList(), - val dex2oatCompatibility: Int = ILSPManagerService.DEX2OAT_OK, + val dex2oatWrapperState: Int = IManagerService.DEX2OAT_OK, val sepolicyLoaded: Boolean = false, val systemServerInjected: Boolean = false, /** @@ -115,7 +115,7 @@ data class ManagerPresence( * second is worth a modal. */ val notificationKnown: Boolean = false, - /** One of the ILSPManagerService.ROOT_* constants, for naming the action button's owner. */ + /** One of the IManagerService.ROOT_* constants, for naming the action button's owner. */ val rootImplementation: Int = 0, ) { /** @@ -314,27 +314,27 @@ class HomeViewModel( } } - private suspend fun refreshStatus(service: ILSPManagerService?) { + private suspend fun refreshStatus(service: IManagerService?) { if (service == null || !daemon.isAlive) { _status.value = FrameworkStatus(state = FrameworkState.Inactive) return } - val versionName = daemon.getXposedVersionName().getOrNull() - val commit = daemon.getFrameworkCommit().getOrNull() + val versionName = daemon.getFrameworkVersionName().getOrNull() + val commit = daemon.getBuildStamp().getOrNull() val versionCode = daemon - .getXposedVersionCode() + .getFrameworkVersionCode() .onFailure { e -> logW("status: framework version code unavailable, update check skipped", e) } .getOrDefault(0L) - val api = daemon.getXposedApiVersion().getOrNull() + val api = daemon.getLibxposedApiVersion().getOrNull() // One line for both, because they fail together on a wedged binder and only these two // defaults synthesise a red HealthIssue card. val sepolicyResult = daemon.isSepolicyLoaded() - val systemServerResult = daemon.systemServerRequested() + val systemServerResult = daemon.isSystemServerAttached() val healthFailure = sepolicyResult.exceptionOrNull() ?: systemServerResult.exceptionOrNull() if (healthFailure != null && healthFailure !is CancellationException) { logW( @@ -345,18 +345,17 @@ class HomeViewModel( } val sepolicy = sepolicyResult.getOrDefault(false) val systemServer = systemServerResult.getOrDefault(false) - val dex2oat = - daemon.getDex2OatWrapperCompatibility().getOrDefault(ILSPManagerService.DEX2OAT_OK) - val dex2oatFlags = daemon.dex2oatFlagsLoaded().getOrDefault(true) + val dex2oat = daemon.getDex2OatWrapperState().getOrDefault(IManagerService.DEX2OAT_OK) + val inliningDisabled = daemon.isDex2OatInliningDisabled().getOrDefault(true) val issues = buildList { if (!sepolicy) add(HealthIssue.SepolicyNotLoaded) if (!systemServer) add(HealthIssue.SystemServerNotInjected) - // The wrapper and the property are alternatives, not a pair: the daemon deletes - // `dalvik.vm.dex2oat-flags` when it mounts the wrapper over dex2oat and sets it when - // it unmounts, so either route suppresses the inlining. A wrapper that is not OK - // therefore only costs anything when the flag did not load either. - if (dex2oat != ILSPManagerService.DEX2OAT_OK && !dex2oatFlags) { + // The wrapper and the property are two routes to one end, not a pair: the daemon + // deletes `dalvik.vm.dex2oat-flags` when it mounts the wrapper over dex2oat and sets + // it when it unmounts, so either route suppresses the inlining. A wrapper that is not + // OK therefore only costs anything when the property is not carrying the flag either. + if (dex2oat != IManagerService.DEX2OAT_OK && !inliningDisabled) { add(HealthIssue.Dex2oatWrapperBroken) } } @@ -369,7 +368,7 @@ class HomeViewModel( versionCode = versionCode, apiVersion = api, issues = issues, - dex2oatCompatibility = dex2oat, + dex2oatWrapperState = dex2oat, sepolicyLoaded = sepolicy, systemServerInjected = systemServer, ) @@ -457,7 +456,7 @@ class HomeViewModel( // that is simply not running. if (!daemon.isAlive) return daemon - .enableStatusNotification() + .isStatusNotificationEnabled() .onSuccess { enabled -> _statusNotification.value = enabled // The notification is one of the ways into the manager, so what the card offers @@ -478,7 +477,7 @@ class HomeViewModel( // Read rather than assumed: this one is a global system setting, so anything on the device // can have moved it since the manager last wrote it. daemon - .forcedLauncherIcons() + .isForcedLauncherIcons() .onSuccess { _hiddenIcon.value = it } .onFailure { e -> logW("status: launcher-icon toggle unread", e) } } @@ -486,7 +485,7 @@ class HomeViewModel( fun setStatusNotification(enabled: Boolean) { viewModelScope.launch { daemon - .setEnableStatusNotification(enabled) + .setStatusNotificationEnabled(enabled) .onSuccess { _statusNotification.value = enabled // Known either way now, which matters when `enabled` is false: someone who @@ -509,7 +508,7 @@ class HomeViewModel( // Read back rather than assumed. The AIDL call returns nothing, and the daemon // applies it by running `settings put global`, which can fail without saying so — // so a transaction that arrived is not yet a setting that changed. - _hiddenIcon.value = daemon.forcedLauncherIcons().getOrDefault(force) + _hiddenIcon.value = daemon.isForcedLauncherIcons().getOrDefault(force) } } } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt index 32227b7f7..0ef54df88 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt @@ -52,7 +52,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import org.lsposed.lspd.ILSPManagerService +import org.matrix.vector.ipc.IManagerService import org.matrix.vector.manager.BuildConfig import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.ErrorOutline @@ -63,13 +63,13 @@ import androidx.compose.ui.draw.alpha import android.content.res.Configuration import java.util.Locale import org.matrix.vector.manager.R -import org.matrix.vector.manager.ui.components.FrameworkState import org.matrix.vector.manager.data.log.CrashRecorder import org.matrix.vector.manager.data.model.ManagerCopy import org.matrix.vector.manager.data.model.XposedApi import org.matrix.vector.manager.data.log.CrashReport import org.matrix.vector.manager.data.model.buildStamp import org.matrix.vector.manager.data.repository.ManagerInstallStep +import org.matrix.vector.manager.ui.components.FrameworkState import org.matrix.vector.manager.ui.components.SnackbarTone import org.matrix.vector.manager.ui.components.VectorSnackbarHost import org.matrix.vector.manager.ui.components.copyToClipboard @@ -485,9 +485,9 @@ private fun InstallFailure(failure: ManagerInstallStep.Failed, onRemoveConflicti @Composable private fun rootManagerName(presence: ManagerPresence): String = when (presence.rootImplementation) { - ILSPManagerService.ROOT_MAGISK -> "Magisk" - ILSPManagerService.ROOT_KERNELSU -> "KernelSU" - ILSPManagerService.ROOT_APATCH -> "APatch" + IManagerService.ROOT_MAGISK -> "Magisk" + IManagerService.ROOT_KERNELSU -> "KernelSU" + IManagerService.ROOT_APATCH -> "APatch" else -> stringResource(R.string.launcher_root_generic) } @@ -804,10 +804,9 @@ private fun buildSections( ), InfoItem( str(R.string.info_dex2oat), - dex2oatLabel(context, status.dex2oatCompatibility), + dex2oatLabel(context, status.dex2oatWrapperState), health = - if (status.dex2oatCompatibility == ILSPManagerService.DEX2OAT_OK) - Health.Good + if (status.dex2oatWrapperState == IManagerService.DEX2OAT_OK) Health.Good else Health.Bad, monospace = false, ), @@ -824,16 +823,14 @@ private fun buildSections( ) } -private fun dex2oatLabel(context: Context, compatibility: Int): String = +private fun dex2oatLabel(context: Context, state: Int): String = context.getString( - when (compatibility) { - ILSPManagerService.DEX2OAT_OK -> R.string.info_supported - ILSPManagerService.DEX2OAT_CRASHED -> R.string.info_dex2oat_crashed - ILSPManagerService.DEX2OAT_MOUNT_FAILED -> R.string.info_dex2oat_mount_failed - ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE -> - R.string.info_dex2oat_selinux_permissive - ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT -> - R.string.info_dex2oat_sepolicy_incorrect + when (state) { + IManagerService.DEX2OAT_OK -> R.string.info_supported + IManagerService.DEX2OAT_CRASHED -> R.string.info_dex2oat_crashed + IManagerService.DEX2OAT_MOUNT_FAILED -> R.string.info_dex2oat_mount_failed + IManagerService.DEX2OAT_SELINUX_PERMISSIVE -> R.string.info_dex2oat_selinux_permissive + IManagerService.DEX2OAT_SEPOLICY_INCORRECT -> R.string.info_dex2oat_sepolicy_incorrect else -> R.string.info_unsupported } ) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsViewModel.kt index 87af5be1d..af036d530 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsViewModel.kt @@ -160,7 +160,7 @@ class LogsViewModel(private val daemon: DaemonClient, private val settings: Sett /** * True when the user asked for verbose logging off and the daemon kept it on. * - * The current daemon's `ManagerService.isVerboseLog()` returns + * The current daemon's `ManagerService.isVerboseLogEnabled()` returns * `PreferenceStore.isVerboseLogEnabled()` unmodified, so this stays false against it. An older * daemon OR'd that preference with its own build type and the switch would snap straight back; * rather than let a control refuse to move with no explanation, the screen reads the value the @@ -260,7 +260,7 @@ class LogsViewModel(private val daemon: DaemonClient, private val settings: Sett } val result = - if (chosen == null) daemon.getLog(verbose) + if (chosen == null) daemon.getLiveLogPart(verbose) else daemon.getLogPart(verbose, chosen) val pfd = result.getOrElse { @@ -544,25 +544,37 @@ class LogsViewModel(private val daemon: DaemonClient, private val settings: Sett /** * Rotates the current log. * - * Named that way because that is what happens: the daemon's `clearLogs()` calls + * Named that way because that is what happens: the daemon's `startNewLogPart()` calls * `LogcatMonitor.refresh()`, which opens a fresh part and leaves the closed one on disk under a * ten-part LRU, still reachable from the part chevrons and still carried by the zip export. * Nothing is truncated, so this reloads and re-indexes rather than emptying anything. + * + * [onResult] reports whether the daemon took the request, which is as much as there is to know: + * the call answers nothing, because the daemon asks its reader to rotate by writing a sentinel + * into the log and never learns whether it acted. Folded on the `Result` itself — it used to + * fold on `getOrDefault(false)` over a `Result` whose boolean was the daemon's + * constant `true`, so the only thing that answer ever reported was that a transaction had + * happened, while reading as though the rotation had. */ fun rotate(tab: LogTab, onResult: (Boolean) -> Unit) { viewModelScope.launch { - val result = daemon.clearLogs(tab == LogTab.VERBOSE) - result.onFailure { - logE("logs: rotating the ${tab.name.lowercase()} log failed", it) - } - val ok = result.getOrDefault(false) - if (ok) reload(tab, Jump.NEWEST) - onResult(ok) + daemon + .startNewLogPart(tab == LogTab.VERBOSE) + .fold( + onSuccess = { + reload(tab, Jump.NEWEST) + onResult(true) + }, + onFailure = { + logE("logs: rotating the ${tab.name.lowercase()} log failed", it) + onResult(false) + }, + ) } } /** - * Writes every log the daemon holds into [uri] as a zip. + * Writes the daemon's bug report into [uri] as a zip — far more than the logs, as below. * * This is the slowest binder transaction on the screen by a wide margin — `FileSystem.getLogs` * walks `/data/tombstones` and `/data/anr`, shells out to `logcat -b all -d` and `dmesg`, @@ -587,7 +599,7 @@ class LogsViewModel(private val daemon: DaemonClient, private val settings: Sett if (fd == null) LogSaveState.Failed(null) else daemon - .writeLogsTo(fd) + .writeBugReportTo(fd) .fold( onSuccess = { LogSaveState.Saved(uri) }, onFailure = { LogSaveState.Failed(it.message) }, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt index e9014e4c8..39b3afe77 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesScreen.kt @@ -106,7 +106,7 @@ import org.matrix.vector.manager.ui.components.SheetHeading import org.matrix.vector.manager.ui.components.sheetRowColors import org.matrix.vector.manager.ui.screens.repo.StoreChannel import org.matrix.vector.manager.ui.screens.repo.releasesOn -import org.lsposed.lspd.ILSPManagerService +import org.matrix.vector.ipc.IManagerService import org.matrix.vector.manager.R import org.matrix.vector.manager.data.model.InstalledModule import org.matrix.vector.manager.di.ServiceLocator @@ -856,11 +856,19 @@ private fun ModuleRow( // Named separately from "could not load it" because it is the one // refusal that is not brokenness: the module is old, and its // author is the only one who can move it forward. - ILSPManagerService.MODULE_LOAD_UNSUPPORTED_API -> + IManagerService.MODULE_LOAD_UNSUPPORTED_API -> R.string.modules_load_unsupported_api - ILSPManagerService.MODULE_LOAD_UNUSABLE -> + IManagerService.MODULE_LOAD_NO_APK -> + R.string.modules_load_no_apk + IManagerService.MODULE_LOAD_UNUSABLE -> R.string.modules_load_unusable - else -> R.string.modules_load_no_apk + // Every other reason, including one this build does not know: + // `ModuleLoadFailure.reason` is never 0, so an unrecognised value + // is a reason a newer daemon has and this manager has not. Saying + // the module could not be loaded is the whole of what is + // established; naming the nearest reason we do know would be a + // guess. + else -> R.string.modules_load_unusable } ), style = MaterialTheme.typography.bodySmall, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt index 3ed1e7156..27b647203 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ModulesViewModel.kt @@ -16,15 +16,14 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.lsposed.lspd.models.Application -import org.lsposed.lspd.models.UserInfo +import org.matrix.vector.ipc.DeviceUser +import org.matrix.vector.ipc.ScopeEntry import org.matrix.vector.manager.data.model.InstalledModule import org.matrix.vector.manager.data.model.MATCH_ANY_USER import org.matrix.vector.manager.data.model.PER_USER_RANGE import org.matrix.vector.manager.data.repository.ModuleRepository import org.matrix.vector.manager.data.model.StoreEntry import org.matrix.vector.manager.data.repository.ModuleUpdateQueue -import org.lsposed.lspd.ILSPManagerService import org.matrix.vector.manager.data.model.XposedApi import org.matrix.vector.manager.data.model.versionCodeCompat import org.matrix.vector.manager.di.ServiceLocator @@ -34,7 +33,7 @@ import org.matrix.vector.manager.logI import org.matrix.vector.manager.logW /** One tab: a user, and the modules installed for them. */ -data class UserModulesState(val user: UserInfo, val modules: List) +data class UserModulesState(val user: DeviceUser, val modules: List) /** What the list is showing. Answering "what is running" should not need a scroll. */ enum class ModuleFilter { @@ -65,11 +64,14 @@ data class ModuleFacts( */ val apiBrokenSince: Int? = null, /** - * Why the framework could not load this module, though it is enabled and installed. + * Why the framework could not load this module, though it is enabled and installed, as one of + * `IManagerService.MODULE_LOAD_*`. * - * Null when it loaded. This is the daemon's two notions of a module disagreeing, and it is the - * only case where a row can be enabled and doing nothing — worth a sentence, because from the - * outside it is indistinguishable from the switch having turned itself off. + * Null when the daemon did not name this module at all, which is every other case: it loaded, + * or it is switched off and there was nothing to load. This is the daemon's two notions of a + * module disagreeing, and it is the only case where a row can be enabled and doing nothing — + * worth a sentence, because from the outside it is indistinguishable from the switch having + * turned itself off. */ val loadFailure: Int? = null, /** @@ -401,18 +403,12 @@ class ModulesViewModel( */ private fun loadFacts(tabs: List) { viewModelScope.launch(Dispatchers.IO) { - val api = daemonClient.getXposedApiVersion().getOrDefault(0) - // One call for the whole list. Asking per module would be one binder round trip per - // row for an answer that is empty on almost every device. - val unloadable = - daemonClient - .getUnloadableModules() - .getOrDefault(emptyList()) - .associateWith { ILSPManagerService.MODULE_LOAD_NO_APK } - .toMutableMap() - unloadable.keys.toList().forEach { pkg -> - daemonClient.getModuleLoadState(pkg).getOrNull()?.let { unloadable[pkg] = it } - } + val api = daemonClient.getLibxposedApiVersion().getOrDefault(0) + // One call for the whole list, package to reason. Absence is the answer for every + // other module — it loaded, or it is switched off and there was nothing to load — so + // nothing here invents a reason for a row the daemon did not name. On a daemon that + // would not answer at all this is empty, which claims nothing about any module. + val loadFailures = daemonClient.getModuleLoadFailures().getOrDefault(emptyMap()) // One lookup table for every scope preview, rather than a package-manager query per // scoped app per module. Keyed by package *and* user: a device with a work profile or // a private space holds the same package twice, and collapsing them would let a row @@ -426,7 +422,7 @@ class ModulesViewModel( // The daemon holds one scope per module package covering every user, so it is read // once here and split per user below — the loop runs over every copy of every module, // because each row needs its own answer. - val scopeCache = mutableMapOf?>() + val scopeCache = mutableMapOf?>() tabs.flatMap { it.modules }.forEach { module -> val scope = scopeCache.getOrPut(module.packageName) { @@ -464,7 +460,7 @@ class ModulesViewModel( apiBrokenSince = if (api <= 0) null else XposedApi.brokenSince(module.apiVersion, api), - loadFailure = unloadable[module.packageName], + loadFailure = loadFailures[module.packageName], scopeFramework = framework, scopePreview = apps diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeViewModel.kt index 3ec5a4dc2..7e9326cc4 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/modules/ScopeViewModel.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.lsposed.lspd.models.Application +import org.matrix.vector.ipc.ScopeEntry import org.matrix.vector.manager.data.model.AppInfo import org.matrix.vector.manager.data.model.ModuleDetection import org.matrix.vector.manager.data.model.RecommendedScope @@ -24,7 +24,14 @@ import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW -/** A package/user pair, as a value type so set arithmetic is correct. */ +/** + * A package/user pair, as a value type so set arithmetic is correct. + * + * Not [ScopeEntry], which carries the same two fields over the wire: it is a generated AIDL bean + * with identity equality, so a set of them would count two readings of the same target as two + * targets and every difference taken here would come out as "everything added and everything + * removed". It is built from these only at the point of writing, in [ScopeViewModel.apply]. + */ data class ScopeTarget(val packageName: String, val userId: Int) /** @@ -819,7 +826,7 @@ class ScopeViewModel( val merged = before + (draft - baseline) - (baseline - draft) val aidl = merged.map { target -> - Application().apply { + ScopeEntry().apply { packageName = target.packageName userId = target.userId } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt index 346f00c5a..62fb8d8d1 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt @@ -99,7 +99,7 @@ fun TroubleshootScreen( // ways this fails: a refused open, no descriptor, and a // failed transaction. checkNotNull(fd) { "no descriptor for the chosen file" } - ServiceLocator.daemon.writeLogsTo(fd).getOrThrow() + ServiceLocator.daemon.writeBugReportTo(fd).getOrThrow() } } .onFailure { e -> diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt index 3463de553..2b4b9e619 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt @@ -76,7 +76,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch -import org.lsposed.lspd.ILSPManagerService +import org.matrix.vector.ipc.IFrameworkInstallReceiver import org.matrix.vector.manager.R import org.matrix.vector.manager.data.repository.FlashStep import org.matrix.vector.manager.ui.screens.repo.StoreHtmlPane @@ -480,9 +480,11 @@ private fun UpdateBar( @Composable private fun failureText(code: Int): String = when (code) { - ILSPManagerService.INSTALL_NO_ROOT -> stringResource(R.string.update_no_root) - ILSPManagerService.INSTALL_NOT_EXECUTED -> stringResource(R.string.update_failed_start) - ILSPManagerService.INSTALL_NO_SUCH_FILE -> stringResource(R.string.update_failed_download) + IFrameworkInstallReceiver.INSTALL_NO_ROOT -> stringResource(R.string.update_no_root) + IFrameworkInstallReceiver.INSTALL_NOT_EXECUTED -> + stringResource(R.string.update_failed_start) + IFrameworkInstallReceiver.INSTALL_NO_SUCH_FILE -> + stringResource(R.string.update_failed_download) else -> stringResource(R.string.update_failed_exit, code) } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt index 67664c6a7..db14e8c33 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.lsposed.lspd.ILSPManagerService +import org.matrix.vector.ipc.IManagerService import org.matrix.vector.manager.data.repository.FlashStep import org.matrix.vector.manager.data.repository.FrameworkUpdateState import org.matrix.vector.manager.di.ServiceLocator @@ -21,16 +21,16 @@ import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW /** Which root implementation is in charge, and whether it can be flashed through. */ -data class RootState(val code: Int = ILSPManagerService.ROOT_UNKNOWN, val version: String? = null) { +data class RootState(val code: Int = IManagerService.ROOT_UNKNOWN, val version: String? = null) { // Named implementations only. ROOT_UNKNOWN is also what a binder proxy returns for a // transaction the daemon does not implement, so it has to refuse rather than guess at an // installer to hand the zip to. val canFlash: Boolean get() = - code == ILSPManagerService.ROOT_MAGISK || - code == ILSPManagerService.ROOT_KERNELSU || - code == ILSPManagerService.ROOT_APATCH + code == IManagerService.ROOT_MAGISK || + code == IManagerService.ROOT_KERNELSU || + code == IManagerService.ROOT_APATCH /** * The sentence to show when flashing is not possible. @@ -43,19 +43,19 @@ data class RootState(val code: Int = ILSPManagerService.ROOT_UNKNOWN, val versio @androidx.compose.runtime.Composable fun label(): String? = when (code) { - ILSPManagerService.ROOT_TOO_OLD -> + IManagerService.ROOT_TOO_OLD -> androidx.compose.ui.res.stringResource( org.matrix.vector.manager.R.string.update_root_too_old ) - ILSPManagerService.ROOT_MULTIPLE -> + IManagerService.ROOT_MULTIPLE -> androidx.compose.ui.res.stringResource( org.matrix.vector.manager.R.string.update_root_multiple ) - ILSPManagerService.ROOT_NONE -> + IManagerService.ROOT_NONE -> androidx.compose.ui.res.stringResource( org.matrix.vector.manager.R.string.update_no_root ) - ILSPManagerService.ROOT_UNKNOWN -> + IManagerService.ROOT_UNKNOWN -> androidx.compose.ui.res.stringResource( org.matrix.vector.manager.R.string.update_root_unknown ) @@ -177,22 +177,22 @@ class FrameworkUpdateViewModel : ViewModel() { viewModelScope.launch { // Two logs for the four requests these blocks make. They all fail from the same // unreachable binder, so only the two that decide what the screen says are recorded; - // the root version and the framework commit take their default in silence. + // the root version and the build stamp take their default in silence. val code = daemon.getRootImplementation().getOrElse { e -> logW("update: root implementation unreadable, screen will say it is unknown", e) - ILSPManagerService.ROOT_UNKNOWN + IManagerService.ROOT_UNKNOWN } val version = daemon.getRootImplementationVersion().getOrNull() _root.value = RootState(code, version) } viewModelScope.launch { val installed = - daemon.getXposedVersionCode().getOrElse { e -> + daemon.getFrameworkVersionCode().getOrElse { e -> logW("update: installed framework version unavailable, update check skipped", e) 0L } - updates.refresh(installed, daemon.getFrameworkCommit().getOrNull()) + updates.refresh(installed, daemon.getBuildStamp().getOrNull()) } } diff --git a/services/daemon-service/build.gradle.kts b/services/daemon-service/build.gradle.kts index 64e28c91c..03030aa9b 100644 --- a/services/daemon-service/build.gradle.kts +++ b/services/daemon-service/build.gradle.kts @@ -13,7 +13,7 @@ android { } aidlPackagedList += "org/matrix/vector/ipc/LoadedModule.aidl" - namespace = "org.lsposed.lspd.daemonservice" + namespace = "org.matrix.vector.daemonservice" } dependencies { 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/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..1d5c2be7d --- /dev/null +++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl @@ -0,0 +1,789 @@ +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 has reached the daemon. + * + *

Latched the moment system_server identifies itself to the bootstrap bridge as uid 1000, + * process {@code system}, with a life token - and never cleared. It is set before that + * process's registration is confirmed, so a registration that then failed still reads true + * here, and the symptom is a status screen claiming the framework is in system_server while no + * module in system_server ever loads. Registration is a map insertion that only fails when the + * life token cannot be linked to death, so that is rare rather than impossible.

+ * + *

False is the answer that matters and it is unambiguous: system_server never got as far as + * the daemon, so the framework is not in it and no module hooking the system will run.

+ */ + 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, and a + * caller that renders this as a supported feature is right for the wrong reason on an old + * device.

+ */ + 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 until the package installer broadcasts its status, with no timeout, so a status + * that never arrives holds a daemon binder thread for the life of the daemon.

+ * + * @return whether the installer reported success. A device-policy refusal and a user that does + * not exist both come back as a plain false, which no exception would show + */ + 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. + * + *

Detected once and cached, because detecting it forks each candidate binary and reads its + * version. 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(); + + /** + * What the root implementation calls itself, for the manager to quote - {@code Magisk 27.0}, + * {@code KernelSU (64e3761d)}, {@code APatch 10762}. + * + *

Not always one version: for {@link #ROOT_MULTIPLE} it is every implementation that + * answered, comma-joined, since the point of that state is that there is more than one.

+ * + * @return null when nothing was detected, or when something was detected and would not say + */ + @nullable String getRootImplementationVersion(); + + /** + * 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; + + /** + * One was found, below the version floor the zygisk loader requires. + * + *

Kept apart from {@link #ROOT_NONE} because the two need different sentences: one asks the + * reader to install a root manager, the other to update the one they have.

+ */ + const int ROOT_TOO_OLD = 2; + + /** + * 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 = 3; + + /** Magisk, at or above the version floor the zygisk loader requires. */ + const int ROOT_MAGISK = 4; + + /** + * KernelSU. + * + *

No version floor is applied, and that is sound rather than a shrug. {@code ksud -V} prints + * a build hash rather than a version code, so there is nothing to compare - the version lives + * behind KernelSU's own prctl interface, which a shell cannot reach. Presence is therefore the + * whole test, and the check that cannot be made here has already been made one layer down: the + * zygisk loader refuses to load on a KernelSU below its floor, so a daemon that is running at + * all is running under one new enough.

+ */ + const int ROOT_KERNELSU = 5; + + /** + * APatch, at or above the version floor the zygisk loader requires - or one whose version + * string this daemon could not parse, which is reported as present rather than as absent. + * Refusing to flash because our own parser did not recognise a version would be refusing on the + * evidence of our code rather than on the state of the device. + */ + const int ROOT_APATCH = 6; +} 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/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..52bd32b4a 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.matrix.vector.ipc.IManagerService import org.lsposed.lspd.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. From f0bb74d20f74a46f90b96830557b52653a80173c Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:39 +0200 Subject: [PATCH 02/13] Move the framework's logger out of org.lsposed.lspd, and out of Utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last of this project's own code under org.lsposed.lspd, and the third item 34144a8b5 left for later. Twenty-two files reach it, across the legacy API, the injected framework and the zygisk bridge, all compiling into one artifact — and nothing in the manager, which wrote its own logger rather than depend on this one. org.matrix.vector.util, deliberately not one of the three prefixes daemon/src/main/jni/obfuscation.cpp rewrites. Nothing here is obfuscated today and this keeps it that way; landing it under .core, .nativebridge or .service would have started rewriting a class the manager does not compile, for no gain. Log becomes a top-level class rather than a member of Utils. It held nothing but static members while being a *non-static inner class*, which Java has accepted only since release 16, and which meant every call site outside an import wrote Utils.Log for a type never scoped to an instance of anything. Splitting it also lets the two say what they are for: Log is the muting-aware stand-in for android.util.Log, and Utils is the convenience layer that logs under the framework's own tag. Which methods honour `muted` is uneven — the message-only forms and w(String, String, Throwable) do, the other Throwable forms do not — and that is carried over exactly. It looks like an oversight, but changing it changes what a muted device records, which is a decision about the log rather than about this move. The tag is the one thing here that must not change: the daemon's log reader routes any tag beginning "Vector" into its verbose stream, so what these helpers write reaches the manager's Verbose tab and travels in an exported bug report. It is now documented where it is declared. XposedBridge keeps its call fully qualified. That file imports android.util.Log too, and it is the framework's own that is wanted there — the platform's getStackTraceString returns an empty string when anything in the cause chain is an UnknownHostException, so a module logging a failed request landed an empty line. --- .../android/xposed/XSharedPreferences.java | 2 +- .../de/robv/android/xposed/XposedBridge.java | 7 +- .../de/robv/android/xposed/XposedInit.java | 2 +- .../main/java/org/matrix/vector/Startup.java | 2 +- .../vector/legacy/LegacyDelegateImpl.java | 2 +- .../java/org/lsposed/lspd/util/Utils.java | 152 ------------------ .../main/java/org/matrix/vector/util/Log.java | 105 ++++++++++++ .../java/org/matrix/vector/util/Utils.java | 67 ++++++++ .../org/matrix/vector/impl/VectorContext.kt | 2 +- .../vector/impl/VectorLifecycleManager.kt | 2 +- .../vector/impl/VectorRemotePreferences.kt | 2 +- .../matrix/vector/impl/core/VectorDeopter.kt | 5 +- .../vector/impl/core/VectorModuleManager.kt | 2 +- .../vector/impl/core/VectorProcessChannel.kt | 2 +- .../vector/impl/core/VectorServiceClient.kt | 2 +- .../matrix/vector/impl/core/VectorStartup.kt | 2 +- .../vector/impl/hookers/CrashDumpHooker.kt | 2 +- .../vector/impl/hookers/LoadedApkHookers.kt | 2 +- .../matrix/vector/impl/hooks/VectorChain.kt | 2 +- .../vector/impl/hooks/VectorNativeHooker.kt | 2 +- .../matrix/vector/ParasiticManagerHooker.kt | 2 +- 21 files changed, 195 insertions(+), 173 deletions(-) delete mode 100644 services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java create mode 100644 services/daemon-service/src/main/java/org/matrix/vector/util/Log.java create mode 100644 services/daemon-service/src/main/java/org/matrix/vector/util/Utils.java 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/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..3841adc9a --- /dev/null +++ b/services/daemon-service/src/main/java/org/matrix/vector/util/Log.java @@ -0,0 +1,105 @@ +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. Which of the remaining overloads honour it is + * uneven — the message-only forms and {@code w(String, String, Throwable)} do, the other + * {@code Throwable} forms do not — and this is carried over unchanged.

+ */ + 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); + } +} 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/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/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index 52bd32b4a..8c6d16a5d 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -24,7 +24,7 @@ import java.io.FileOutputStream import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import org.matrix.vector.ipc.IManagerService -import org.lsposed.lspd.util.Utils +import org.matrix.vector.util.Utils import org.matrix.vector.impl.core.VectorServiceClient /** The "Parasite" logic. Injects the manager APK into a host process (shell). */ From 952ad190dd430a31265f8639181cb416ece22072 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:39 +0200 Subject: [PATCH 03/13] Make the daemon's service classes say which interface they implement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fourth item 34144a8b5 left for later. Renaming the AIDL moved the interfaces to honest names and left the classes implementing them behind. ApplicationService implemented IFrameworkService and named neither: nothing in it is about an Application. It is FrameworkService. The two module services were worse than mismatched, they were swapped in the reader's head. A module gets two services — libxposed's IXposedService, which is the module as its own *app* sees it, and this project's IModuleService, which is the module as an injected *process* sees it — and they deliberately differ in what they permit: an 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. The class implementing IXposedService was called ModuleService, the vaguer of the two names, and the class implementing IModuleService was called InjectedModuleService. The one name that said which of the two it was sat on the wrong one, and the natural name for the other was already taken. ModuleAppService and InjectedModuleService now both carry the distinguishing fact, and each points at the other. That beats dropping the I from each interface, which would give ModuleService and XposedService — two names told apart only by knowing which project each interface belongs to. The daemon AIDL gets the three fixes that reading it for this turned up: - IVectorDaemon.attachProcess returns null in four cases and documented one of them. The others are a caller that is not uid 1000, a (uid, pid) that has already attached, and a registration that failed. @nullable is documentation-only in the Java backend — no annotation is emitted and the signature is unchanged — so this is the doc catching up with the code, not a wire change. - LoadedModule.applicationInfo was the one field in that file with no comment. It is the *module's* ApplicationInfo, not the host process's, which is why it has to be carried rather than looked up. - IVectorDaemon and IModuleService said nothing about transaction numbering while IFrameworkService explains it at length. Both are implicit and append-only for the same reason — they only ever cross between a daemon and a framework dex from one zip — and both now say so, and why IManagerService is explicit instead. --- daemon/README.md | 8 ++-- ...licationService.kt => FrameworkService.kt} | 16 +++++-- .../daemon/ipc/InjectedModuleService.kt | 9 +++- .../{ModuleService.kt => ModuleAppService.kt} | 44 ++++++++++++------- .../org/matrix/vector/ipc/IModuleService.aidl | 8 +++- .../org/matrix/vector/ipc/IVectorDaemon.aidl | 17 +++++-- .../org/matrix/vector/ipc/LoadedModule.aidl | 9 ++++ 7 files changed, 83 insertions(+), 28 deletions(-) rename daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/{ApplicationService.kt => FrameworkService.kt} (95%) rename daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/{ModuleService.kt => ModuleAppService.kt} (91%) 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/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/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/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IModuleService.aidl b/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IModuleService.aidl index 2b08f57e1..f1c436c97 100644 --- a/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IModuleService.aidl +++ b/services/daemon-service/src/main/aidl/org/matrix/vector/ipc/IModuleService.aidl @@ -8,7 +8,13 @@ import org.matrix.vector.ipc.IRemotePreferenceCallback; *

Not to be confused with {@code io.github.libxposed.service.IXposedService}, which is the same * module's service as seen from its app. The two deliberately differ: an 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.

+ * was injected into rather than as the module. The daemon implements this one in + * {@code InjectedModuleService} and that one in {@code ModuleAppService}, so which is which is + * legible from the class name rather than from the import list.

+ * + *

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. */ From 6cf2b4ed66b414899aea1b5d15136c3bb193ec80 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 04/13] Drop the per-file licence headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight files carried the GPL boilerplate at the top — the "this program is free software, distributed without warranty, you should have received a copy" paragraph — inherited from upstream and by then naming a project this is not. The licence is conveyed where it counts and is not weakened by this: LICENSE holds the full GPL-3.0 text at the root, and README's own License section says the project is under it. Per-file headers are the FSF's recommendation for making the terms travel with a stray copy of a single file, not a condition of the licence, and twenty lines of boilerplate above a four-line hidden-API stub was never buying that. What the headers held that nothing else did is EdXposed, named in a copyright line on the logger and nowhere else in the repository. It goes to README's Credits beside LSPosed, which already stood there as upstream source — the lineage is a fact about where this code came from and belongs somewhere a reader will find it. --- README.md | 1 + .../src/main/java/hidden/HiddenApiBridge.java | 19 ------------------- .../java/android/app/IActivityManager.java | 19 ------------------- .../java/android/app/IApplicationThread.java | 19 ------------------- .../main/java/android/app/ProfilerInfo.java | 19 ------------------- .../java/android/content/IIntentReceiver.java | 19 ------------------- .../src/main/java/android/content/Intent.java | 19 ------------------- .../src/main/java/android/os/Bundle.java | 19 ------------------- .../src/main/AndroidManifest.xml | 19 ------------------- 9 files changed, 1 insertion(+), 152 deletions(-) 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/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/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 @@ - - From c0bfd4b2517bdcc71aa502d68f9040c8e16d610b Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 05/13] Report a manager/framework version mismatch instead of "not activated" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not part of the rename; it is what the rename makes necessary, and it can be dropped on its own if it is not wanted. The interface's fully qualified name is its binder descriptor, so moving the AIDL out of org.lsposed.lspd changes it. Two of the three consumers ship in the same zip as the daemon and are always in step. The third is not: getManagerApk exists so the manager can be installed as an ordinary app, and an installed copy survives every later flash. Across that skew nothing is loud. Stub.asInterface wraps any binder in a proxy without checking, the binder stays alive so isBinderAlive() keeps answering true, and every transaction throws SecurityException out of the daemon's enforceInterface — which DaemonClient turns into a failed Result and every screen draws as empty. The header would then read "Not activated", which is the one thing that is certainly false: a framework is plainly running, it pushed us the binder, and the reader is being sent to install something they already have. So Constants.setBinder asks the binder what it is before binding. That question is exempt by construction — INTERFACE_TRANSACTION sits outside the range the generated dispatcher checks the interface token for — so it is answered across any mismatch. It can still throw, since the daemon may have died in between, and a throw is not evidence of a mismatch, so it falls through to binding and lets linkToDeath report the death. FrameworkState gains Mismatched, rendered as "Version mismatch". It keeps the error colours — this is a broken install — but takes the priority glyph rather than the cross, because the cross means absence. The label does not blame either side: the descriptor says which build is which, and that goes to the log, but a user cannot act on the difference and the manager is as likely to be the newer one. The two places asking `state != Inactive` to mean "there is a daemon to talk to" would both have answered wrongly, so the question moved onto FrameworkStatus as daemonUsable. HomeViewModel collects the binder and the mismatch together, because a refusal leaves service exactly as it was — null — so a StateFlow has nothing to emit and the header would sit on whatever it already said. There is no route out from inside a stale manager: the call that would fetch a replacement APK is a transaction on the interface that does not match. The parasitic manager ships with the daemon, is always in step, and is reachable from the status notification and the dialer code. A manager built before this check exists cannot show it at all, and every released build is one — on the flash that lands this, such a copy shows empty screens, as it does today for any other daemon failure. --- .../org/matrix/vector/manager/Constants.kt | 47 ++++++++++++++++- .../vector/manager/di/ServiceLocator.kt | 30 +++++++++++ .../manager/ui/components/StatusHeader.kt | 17 ++++++- .../manager/ui/screens/home/HomeScreen.kt | 6 +-- .../manager/ui/screens/home/HomeViewModel.kt | 50 ++++++++++++++----- .../ui/screens/home/SystemStatusScreen.kt | 3 +- manager/src/main/res/values-ar/strings.xml | 1 + manager/src/main/res/values-de/strings.xml | 1 + manager/src/main/res/values-es/strings.xml | 1 + manager/src/main/res/values-fa/strings.xml | 1 + manager/src/main/res/values-fr/strings.xml | 1 + manager/src/main/res/values-in/strings.xml | 1 + manager/src/main/res/values-it/strings.xml | 1 + manager/src/main/res/values-iw/strings.xml | 1 + manager/src/main/res/values-ja/strings.xml | 1 + manager/src/main/res/values-ko/strings.xml | 1 + manager/src/main/res/values-pl/strings.xml | 1 + .../src/main/res/values-pt-rBR/strings.xml | 1 + manager/src/main/res/values-ru/strings.xml | 1 + manager/src/main/res/values-tr/strings.xml | 1 + manager/src/main/res/values-uk/strings.xml | 1 + manager/src/main/res/values-vi/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + manager/src/main/res/values/strings.xml | 2 + 25 files changed, 154 insertions(+), 19 deletions(-) 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 790119959..89d09ea24 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt @@ -29,7 +29,52 @@ object Constants { @JvmStatic fun setBinder(binder: IBinder): Boolean { - ServiceLocator.bind(IManagerService.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/di/ServiceLocator.kt b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt index dc1ca9c7e..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 @@ -77,6 +77,23 @@ object ServiceLocator { */ 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() = appContext @@ -283,5 +300,18 @@ object ServiceLocator { /** Called from `Constants.setBinder`, possibly before [attach]. */ 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/ui/components/StatusHeader.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/StatusHeader.kt index 237a595c2..51b85b60c 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/StatusHeader.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/StatusHeader.kt @@ -51,12 +51,22 @@ import org.matrix.vector.manager.R import org.matrix.vector.manager.ui.components.ambience.AmbienceKind import org.matrix.vector.manager.ui.components.ambience.AmbientSurface -/** The three states the framework can be in, plus the moment before we know. */ +/** The four states the framework can be in, plus the moment before we know. */ enum class FrameworkState { Checking, Active, Degraded, Inactive, + + /** + * The framework is running, and this manager cannot talk to it. + * + * Distinct from [Inactive], which means there is no framework. Here there is one, it pushed + * us a binder, and that binder speaks a different generation of `IManagerService` — so every + * transaction would fail and the honest thing to say is that the two builds are out of step, + * not that nothing is installed. Reached only through `ServiceLocator.peerDescriptor`. + */ + Mismatched, } /** @@ -96,6 +106,7 @@ fun StatusHeader( FrameworkState.Active -> colors.primaryContainer FrameworkState.Degraded -> colors.tertiaryContainer FrameworkState.Inactive -> colors.errorContainer + FrameworkState.Mismatched -> colors.errorContainer FrameworkState.Checking -> colors.surfaceContainer }, animationSpec = tween(420), @@ -107,6 +118,7 @@ fun StatusHeader( FrameworkState.Active -> colors.onPrimaryContainer FrameworkState.Degraded -> colors.onTertiaryContainer FrameworkState.Inactive -> colors.onErrorContainer + FrameworkState.Mismatched -> colors.onErrorContainer FrameworkState.Checking -> colors.onSurfaceVariant }, animationSpec = tween(420), @@ -121,6 +133,7 @@ fun StatusHeader( FrameworkState.Active -> R.string.status_active FrameworkState.Degraded -> R.string.status_degraded FrameworkState.Inactive -> R.string.status_inactive + FrameworkState.Mismatched -> R.string.status_mismatched FrameworkState.Checking -> R.string.status_checking } ) @@ -317,6 +330,8 @@ private fun StatusIndicator( FrameworkState.Active -> Icons.Rounded.Check FrameworkState.Degraded -> Icons.Rounded.PriorityHigh FrameworkState.Inactive -> Icons.Rounded.Close + // Not a Close: the framework is there. It is this manager that cannot reach it. + FrameworkState.Mismatched -> Icons.Rounded.PriorityHigh FrameworkState.Checking -> null } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt index 49d2631fc..26ab63e24 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt @@ -89,7 +89,6 @@ import org.matrix.vector.manager.ui.theme.LocalizedOverlay import org.matrix.vector.manager.R import org.matrix.vector.manager.ui.theme.currentLocale import org.matrix.vector.manager.di.ServiceLocator -import org.matrix.vector.manager.ui.components.FrameworkState import org.matrix.vector.manager.ui.components.VectorAlertDialog import org.matrix.vector.manager.ui.components.VectorSnackbarHost import org.matrix.vector.manager.ui.components.show @@ -340,8 +339,9 @@ fun HomeScreen( showLauncherPrompt && presence.unreachable && !promptDismissed && - // With no daemon there is no APK to install and bigger problems to report first. - status.state != FrameworkState.Inactive + // With no usable daemon there is no APK to install and bigger problems to report + // first. + status.daemonUsable ) { LauncherPrompt( shortcutSupported = presence.shortcutSupported, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt index 90d3e27c7..d9d699658 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt @@ -59,6 +59,18 @@ data class FrameworkStatus( ) { val versionLabel: String? get() = versionName?.let { if (versionCode > 0) "$it ($versionCode)" else it } + + /** + * Whether there is a daemon this manager can actually ask anything. + * + * Not `state != Inactive`, which is what the two callers used to test and which + * [FrameworkState.Mismatched] would answer wrongly: there a framework is plainly running and + * simply speaking a generation of the interface this build does not, so every transaction + * fails. Anything gated on being able to *use* the daemon belongs here rather than on a + * comparison a later state can slip past. + */ + val daemonUsable: Boolean + get() = state != FrameworkState.Inactive && state != FrameworkState.Mismatched } /** @@ -281,17 +293,23 @@ class HomeViewModel( // The binder may arrive after this ViewModel exists — injection order is not ours to // control — so status is re-derived whenever it changes rather than read once in init. viewModelScope.launch { - ServiceLocator.service.collect { service -> - refreshStatus(service) - // Both switches on the status page hold the daemon's state rather than ours, and - // the binder is what they need. This runs for every binder, including one already - // in hand when `refreshPresence` ran above — a second read of two idempotent - // values — and it is here for the one that arrives afterwards, which that call - // found nothing to ask about and returned. Nor is it the last such moment: - // `refreshPresence` asks again whenever a screen that shows them is opened, since - // this flow does not emit a second time while one binder stays alive. - if (service != null) refreshToggles() - } + // Both flows, because a refused binder leaves `service` null and only moves + // `peerDescriptor`. Collecting `service` alone would see no change at all — it was + // already null — and the header would sit on "not activated" for a framework that is + // running and simply out of step with this build. + combine(ServiceLocator.service, ServiceLocator.peerMismatch) { service, _ -> service } + .collect { service -> + refreshStatus(service) + // Both switches on the status page hold the daemon's state rather than ours, + // and the binder is what they need. This runs for every binder, including one + // already in hand when `refreshPresence` ran above — a second read of two + // idempotent values — and it is here for the one that arrives afterwards, + // which that call found nothing to ask about and returned. Nor is it the last + // such moment: `refreshPresence` asks again whenever a screen that shows them + // is opened, since this flow does not emit a second time while one binder + // stays alive. + if (service != null) refreshToggles() + } } // Opening Home is not a reason to talk to GitHub. The page renders from disk every time // and only occasionally goes and checks — the window it shows changes a few times a week @@ -316,7 +334,15 @@ class HomeViewModel( private suspend fun refreshStatus(service: IManagerService?) { if (service == null || !daemon.isAlive) { - _status.value = FrameworkStatus(state = FrameworkState.Inactive) + // A binder did arrive and was refused for speaking a different generation of the + // interface, which is not the same thing as there being no framework — and saying "not + // activated" for it sends the reader to reinstall something that is already running. + val mismatch = ServiceLocator.peerMismatch.value + _status.value = + FrameworkStatus( + state = + if (mismatch != null) FrameworkState.Mismatched else FrameworkState.Inactive + ) return } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt index 0ef54df88..14b66637a 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt @@ -69,7 +69,6 @@ import org.matrix.vector.manager.data.model.XposedApi import org.matrix.vector.manager.data.log.CrashReport import org.matrix.vector.manager.data.model.buildStamp import org.matrix.vector.manager.data.repository.ManagerInstallStep -import org.matrix.vector.manager.ui.components.FrameworkState import org.matrix.vector.manager.ui.components.SnackbarTone import org.matrix.vector.manager.ui.components.VectorSnackbarHost import org.matrix.vector.manager.ui.components.copyToClipboard @@ -126,7 +125,7 @@ fun SystemStatusScreen( // screen, because the process that would record it is the one drawing it. var crash by remember { mutableStateOf(CrashRecorder.newest(context)) } // The two switches below belong to the framework, so they are only live while it is. - val daemonAlive = status.state != FrameworkState.Inactive + val daemonAlive = status.daemonUsable val snackbars = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val copied = stringResource(R.string.copied) diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 0487d8f8f..78681d8c1 100644 --- a/manager/src/main/res/values-ar/strings.xml +++ b/manager/src/main/res/values-ar/strings.xml @@ -17,6 +17,7 @@ نشط يحتاج فحصًا غير مُفعَّل + عدم تطابق الإصدار جارٍ الفحص… فتح حالة النظام diff --git a/manager/src/main/res/values-de/strings.xml b/manager/src/main/res/values-de/strings.xml index 151f6f3f3..e349e78d2 100644 --- a/manager/src/main/res/values-de/strings.xml +++ b/manager/src/main/res/values-de/strings.xml @@ -17,6 +17,7 @@ Aktiv Prüfen nötig Nicht aktiviert + Versionskonflikt Wird geprüft… Systemstatus öffnen diff --git a/manager/src/main/res/values-es/strings.xml b/manager/src/main/res/values-es/strings.xml index 131d2daca..fbb299ee0 100644 --- a/manager/src/main/res/values-es/strings.xml +++ b/manager/src/main/res/values-es/strings.xml @@ -17,6 +17,7 @@ Activo Revisar Sin activar + Versiones incompatibles Comprobando… Abrir el estado del sistema diff --git a/manager/src/main/res/values-fa/strings.xml b/manager/src/main/res/values-fa/strings.xml index 93ae815f6..b1bb3d0ce 100644 --- a/manager/src/main/res/values-fa/strings.xml +++ b/manager/src/main/res/values-fa/strings.xml @@ -17,6 +17,7 @@ فعال نیازمند توجه فعال نشده + ناسازگاری نسخه در حال بررسی… باز کردن وضعیت سامانه diff --git a/manager/src/main/res/values-fr/strings.xml b/manager/src/main/res/values-fr/strings.xml index 14b789423..8b831df17 100644 --- a/manager/src/main/res/values-fr/strings.xml +++ b/manager/src/main/res/values-fr/strings.xml @@ -17,6 +17,7 @@ Actif À vérifier Non activé + Versions incompatibles Vérification… Ouvrir l\'état du système diff --git a/manager/src/main/res/values-in/strings.xml b/manager/src/main/res/values-in/strings.xml index f395140aa..dd95b761a 100644 --- a/manager/src/main/res/values-in/strings.xml +++ b/manager/src/main/res/values-in/strings.xml @@ -21,6 +21,7 @@ Aktif Perlu dicek Belum diaktifkan + Versi tidak cocok Memeriksa… Buka status sistem diff --git a/manager/src/main/res/values-it/strings.xml b/manager/src/main/res/values-it/strings.xml index 8461d27ed..9acc62eba 100644 --- a/manager/src/main/res/values-it/strings.xml +++ b/manager/src/main/res/values-it/strings.xml @@ -17,6 +17,7 @@ Attivo Da verificare Non attivato + Versioni incompatibili Controllo… Apri lo stato del sistema diff --git a/manager/src/main/res/values-iw/strings.xml b/manager/src/main/res/values-iw/strings.xml index 96eb0f692..3a7a38d48 100644 --- a/manager/src/main/res/values-iw/strings.xml +++ b/manager/src/main/res/values-iw/strings.xml @@ -21,6 +21,7 @@ פעיל דורש בדיקה לא הופעל + אי-התאמת גרסאות בודק… פתיחת מצב המערכת diff --git a/manager/src/main/res/values-ja/strings.xml b/manager/src/main/res/values-ja/strings.xml index 6aedb1541..a0543c0ba 100644 --- a/manager/src/main/res/values-ja/strings.xml +++ b/manager/src/main/res/values-ja/strings.xml @@ -17,6 +17,7 @@ 有効 確認が必要 未有効化 + バージョン不一致 確認中… システム状態を開く diff --git a/manager/src/main/res/values-ko/strings.xml b/manager/src/main/res/values-ko/strings.xml index f761eb44c..c1d79d578 100644 --- a/manager/src/main/res/values-ko/strings.xml +++ b/manager/src/main/res/values-ko/strings.xml @@ -17,6 +17,7 @@ 활성 확인 필요 활성화되지 않음 + 버전 불일치 확인 중… 시스템 상태 열기 diff --git a/manager/src/main/res/values-pl/strings.xml b/manager/src/main/res/values-pl/strings.xml index 13fea0ea0..ca419df13 100644 --- a/manager/src/main/res/values-pl/strings.xml +++ b/manager/src/main/res/values-pl/strings.xml @@ -17,6 +17,7 @@ Aktywny Problemy Nieaktywowany + Niezgodność wersji Sprawdzanie… Otwórz stan systemu diff --git a/manager/src/main/res/values-pt-rBR/strings.xml b/manager/src/main/res/values-pt-rBR/strings.xml index a2bf0761e..be30ea165 100644 --- a/manager/src/main/res/values-pt-rBR/strings.xml +++ b/manager/src/main/res/values-pt-rBR/strings.xml @@ -17,6 +17,7 @@ Ativo Verificar Não ativado + Versões incompatíveis Verificando… Abrir o status do sistema diff --git a/manager/src/main/res/values-ru/strings.xml b/manager/src/main/res/values-ru/strings.xml index 21ac520a7..97b963393 100644 --- a/manager/src/main/res/values-ru/strings.xml +++ b/manager/src/main/res/values-ru/strings.xml @@ -15,6 +15,7 @@ Активен Проблемы Не активирован + Несовпадение версий Проверка… Открыть состояние системы Политика SELinux не загружена diff --git a/manager/src/main/res/values-tr/strings.xml b/manager/src/main/res/values-tr/strings.xml index a3fdcff07..0f0f1856b 100644 --- a/manager/src/main/res/values-tr/strings.xml +++ b/manager/src/main/res/values-tr/strings.xml @@ -17,6 +17,7 @@ Etkin Sorun var Etkinleştirilmedi + Sürüm uyuşmazlığı Denetleniyor… Sistem durumunu aç diff --git a/manager/src/main/res/values-uk/strings.xml b/manager/src/main/res/values-uk/strings.xml index e048b19ea..1bd30571b 100644 --- a/manager/src/main/res/values-uk/strings.xml +++ b/manager/src/main/res/values-uk/strings.xml @@ -17,6 +17,7 @@ Активний Проблеми Не активовано + Розбіжність версій Перевірка… Відкрити стан системи diff --git a/manager/src/main/res/values-vi/strings.xml b/manager/src/main/res/values-vi/strings.xml index 06ea53c70..c8bcd0d27 100644 --- a/manager/src/main/res/values-vi/strings.xml +++ b/manager/src/main/res/values-vi/strings.xml @@ -17,6 +17,7 @@ Đang hoạt động Cần kiểm tra Chưa kích hoạt + Phiên bản không khớp Đang kiểm tra… Mở trạng thái hệ thống diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index ef4246918..5e34315ee 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -17,6 +17,7 @@ 已激活 异常 未激活 + 版本不匹配 正在检查… 查看系统状态 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index a49872eb5..0aadbdf24 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -17,6 +17,7 @@ 已啟用 異常 未啟用 + 版本不符 正在檢查… 檢視系統狀態 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index f247b4dcf..922caa631 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -30,6 +30,8 @@ Active Needs attention Not activated + + Version mismatch Checking… Open system status From 6195ac70d65ca5334d40b6dd7cdb7aa10a935bf1 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 06/13] Stop reporting a module row that was never inserted as a success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `enableModule` has two paths. Updating an existing row checks what it did — `changed = db.update(...) > 0` — and inserting a newly discovered module did not: it called `db.insert(...)`, threw the result away, and set `changed = true`. `SQLiteDatabase.insert` does not throw on failure. It catches the SQLException itself, logs one line, and answers -1. A disk that is full, a database locked by a concurrent write, or a constraint that rejects the row therefore all looked exactly like a successful insert. Everything downstream then acted on that. `setModuleEnabled` answered true, so the manager left its switch on. `ConfigCache.requestCacheUpdate()` ran for a module that has no row. Worst of the three, the shade's "this module is not activated yet" notice was cancelled — the one thing that would have told the user something was wrong. The module then simply never loaded, with the manager showing it enabled and nothing anywhere reporting a failure. --- .../kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 1c6611709..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 @@ -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 From 0d224da7aa7c9c541658b80a9fe39cae7af6d915 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 07/13] Latch system_server as attached only once it has been served MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `systemServerRequested` was set immediately after the uid/process-name/life-token gate, before `FrameworkService.registerHeartBeat` — the call that decides whether system_server actually gets its framework service. Registration answers false when the life token cannot be linked to death, which is rare rather than impossible, and the flag is never cleared. So a failed registration read as attached, and the manager's status page reported the framework as present in system_server while no module hooking the system loaded. That is the expensive kind of wrong: a green health row sends the reader to look at their module, which is the one place the fault is not. The assignment moves below the registration, so the flag now means what `isSystemServerAttached()` says it means. The observable change is that the health row flips a moment later during boot — after the registration rather than at the request — and stays false on the failure it used to hide. --- .../vector/daemon/ipc/SystemServerService.kt | 14 +++++++---- .../matrix/vector/ipc/IManagerService.aidl | 23 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) 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 2b67084b7..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 FrameworkService singleton if successfully registered - return if (FrameworkService.registerHeartBeat(uid, pid, processName, processLifeToken)) { - FrameworkService - } 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 { 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 index 1d5c2be7d..323ba31aa 100644 --- 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 @@ -140,17 +140,18 @@ interface IManagerService { // ---- whether the framework is actually working ---------------------------------------------- /** - * Whether system_server has reached the daemon. - * - *

Latched the moment system_server identifies itself to the bootstrap bridge as uid 1000, - * process {@code system}, with a life token - and never cleared. It is set before that - * process's registration is confirmed, so a registration that then failed still reads true - * here, and the symptom is a status screen claiming the framework is in system_server while no - * module in system_server ever loads. Registration is a map insertion that only fails when the - * life token cannot be linked to death, so that is rare rather than impossible.

- * - *

False is the answer that matters and it is unambiguous: system_server never got as far as - * the daemon, so the framework is not in it and no module hooking the system will run.

+ * 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(); From 5c50b2494f1fa73b99ceb66fb6dba5a1ffe82eb7 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 08/13] Bound the wait for an uninstall status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uninstallPackage` runs on a binder thread, asks the package installer to remove a package, and then waited on a CountDownLatch with no timeout for the status broadcast. The status is not guaranteed to arrive. A device-policy refusal, a user removed while the uninstall is in flight, or a wedged package installer all end with nobody counting the latch down. That thread is then held for the life of the daemon. The cost is not confined to the caller. The daemon's binder thread pool is small, and threads lost this way never come back, so enough of them starve everything else the daemon answers — the manager's module list, the log reads, and the calls injected processes make while they start. The symptom is a manager that hangs on every screen and a device that needs the daemon restarted, none of which points at an uninstall somebody tried a while ago. A minute, which is generous for work that normally takes seconds, and false on expiry: a timeout is a failure of our knowledge rather than of the uninstall, and the caller must not be told a package is gone on the strength of a status that never came. --- .../vector/daemon/ipc/ManagerService.kt | 24 +++++++++++++++++-- .../matrix/vector/ipc/IManagerService.aidl | 13 ++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) 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 cfa5bdfc0..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,6 +24,7 @@ import hidden.HiddenApiBridge import io.github.libxposed.service.IXposedService import java.io.File import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import org.matrix.vector.ipc.DeviceUser import org.matrix.vector.ipc.IFrameworkInstallReceiver import org.matrix.vector.ipc.IManagerService @@ -52,6 +53,14 @@ 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 @@ -384,7 +393,19 @@ object ManagerService : IManagerService.Stub() { 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 } @@ -508,7 +529,6 @@ object ManagerService : IManagerService.Stub() { override fun getRootImplementation() = RootImplementation.implementation - override fun getRootImplementationVersion() = RootImplementation.version override fun installFrameworkZip(zipPath: String, receiver: IFrameworkInstallReceiver) { // Off the binder thread: a flash takes seconds to minutes, and holding a binder thread for its 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 index 323ba31aa..6242d8249 100644 --- 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 @@ -583,11 +583,14 @@ interface IManagerService { * needs to replace itself: a copy left behind in another profile refuses an install exactly as * loudly as one in this profile.

* - *

Blocks until the package installer broadcasts its status, with no timeout, so a status - * that never arrives holds a daemon binder thread for the life of the daemon.

- * - * @return whether the installer reported success. A device-policy refusal and a user that does - * not exist both come back as a plain false, which no exception would show + *

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); From f3d9c563944b94f4ee346d2f85dc46a68b93d166 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 09/13] Honour the log mute on the overloads that carry a throwable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `muted` gated the message-only forms of d/v/i and both forms of w, but not d/v/i(String, String, Throwable). The setting therefore only half worked: a user who turned verbose logging off still got every debug, verbose and info line that happened to carry an exception. Those are not the rare ones. `muted` is set in injected processes, from `IFrameworkService.isLogMuted`, and the lines that carry a throwable are the ones on the failure paths of hooking — the paths that run in every process the framework touches. A muted device kept paying for them, and kept writing them to a log the user had asked to be quiet. ERROR and above stay ungated, deliberately and as before: muting asks for less noise, not for a failure to go unrecorded. --- .../src/main/java/org/matrix/vector/util/Log.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 index 3841adc9a..9ffd4cfad 100644 --- 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 @@ -31,9 +31,12 @@ public class Log { * *

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. Which of the remaining overloads honour it is - * uneven — the message-only forms and {@code w(String, String, Throwable)} do, the other - * {@code Throwable} forms do not — and this is carried over unchanged.

+ * 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; @@ -64,6 +67,7 @@ public static void d(String tag, String msg) { } public static void d(String tag, String msg, Throwable tr) { + if (muted) return; android.util.Log.d(tag, msg, tr); } @@ -73,6 +77,7 @@ public static void v(String tag, String msg) { } public static void v(String tag, String msg, Throwable tr) { + if (muted) return; android.util.Log.v(tag, msg, tr); } @@ -82,6 +87,7 @@ public static void i(String tag, String msg) { } public static void i(String tag, String msg, Throwable tr) { + if (muted) return; android.util.Log.i(tag, msg, tr); } From 95393d260de0c3f23953a1b9e90db0e1a9b26a57 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 10/13] Leave the dex2oat row out on releases that have no wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon only starts the dex2oat wrapper machinery from Android 10 and answers a literal 0 below it — and 0 is DEX2OAT_OK. The status page rendered that as "Supported", in green, with a healthy tick. On Android 8.1 and 9 that is a positive claim about a feature the device does not have. The platform's dex2oat inlines across methods there exactly as it always did, so a hook on an inlined method does not take, and a reader chasing that is told by the status page that this part is fine. The row is dropped below Android 10 rather than given a fifth state. A new constant was the other option and it renders worse: every value the screen does not recognise falls to "Unsupported", so a device that today reads a wrong green would have read a wrong red, which is further from the truth rather than closer. Saying nothing about a wrapper that does not exist is the honest answer, and the section still carries SELinux and system_server, which do apply there. No health issue changes: `Dex2oatWrapperBroken` is raised only when the state is not DEX2OAT_OK, so it was never raised below Android 10 either. --- .../ui/screens/home/SystemStatusScreen.kt | 26 ++++++++++++------- .../matrix/vector/ipc/IManagerService.aidl | 6 ++--- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt index 14b66637a..87214af1a 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt @@ -1,5 +1,6 @@ package org.matrix.vector.manager.ui.screens.home +import android.os.Build import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -782,7 +783,7 @@ private fun buildSections( InfoItem(str(R.string.info_manager_package), context.packageName), ), str(R.string.info_section_health) to - listOf( + listOfNotNull( InfoItem( str(R.string.info_selinux), str( @@ -801,14 +802,21 @@ private fun buildSections( health = if (status.systemServerInjected) Health.Good else Health.Bad, monospace = false, ), - InfoItem( - str(R.string.info_dex2oat), - dex2oatLabel(context, status.dex2oatWrapperState), - health = - if (status.dex2oatWrapperState == IManagerService.DEX2OAT_OK) Health.Good - else Health.Bad, - monospace = false, - ), + // Omitted below Android 10, where there is no wrapper to report on: the daemon only + // starts that machinery from Q and answers DEX2OAT_OK before then, so the row read + // "Supported", in green, for a feature the device does not have. A reader chasing a + // module that will not hook was being told this part was fine. + if (device.sdkInt < Build.VERSION_CODES.Q) null + else + InfoItem( + str(R.string.info_dex2oat), + dex2oatLabel(context, status.dex2oatWrapperState), + health = + if (status.dex2oatWrapperState == IManagerService.DEX2OAT_OK) + Health.Good + else Health.Bad, + monospace = false, + ), ), str(R.string.info_section_device) to listOf( 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 index 6242d8249..3760f80a3 100644 --- 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 @@ -196,9 +196,9 @@ interface IManagerService { * *

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, and a - * caller that renders this as a supported feature is right for the wrong reason on an old - * device.

+ * 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; From aa1f37bdf3054f068fadff00e7694f79d7c3b48c Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 11/13] Retire getRootImplementationVersion, which nothing ever rendered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FrameworkUpdateViewModel` fetched it into `RootState.version` on every refresh of the framework update screen, and no composable read that field. It was one binder transaction per refresh buying nothing, and it was the only consumer the AIDL method had — so the method had no readers at all. Removed rather than rendered because rendering it is a design decision about that screen, not a defect to repair. The daemon still derives the string and still logs it (`Root implementation: Magisk 27.0 via …`, and the comma-joined list for ROOT_MULTIPLE), so nothing is lost from a bug report; only the unread IPC is gone. Removing a method shifts the transaction id of every one below it, which is exactly what PROTOCOL_VERSION is there to make safe: a peer that has not been rebuilt is refused at the handshake rather than left calling a number that now means something else. --- .../matrix/vector/daemon/utils/RootImplementation.kt | 3 --- .../org/matrix/vector/manager/demo/DemoScenario.kt | 6 ------ .../matrix/vector/manager/demo/FakeManagerService.kt | 1 - .../org/matrix/vector/manager/ipc/DaemonClient.kt | 4 ---- .../ui/screens/update/FrameworkUpdateViewModel.kt | 11 +++++------ .../aidl/org/matrix/vector/ipc/IManagerService.aidl | 11 ----------- 6 files changed, 5 insertions(+), 31 deletions(-) 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 ec04f6020..8b964eecf 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 @@ -65,9 +65,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() 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 6f5e1cb8b..d9b7e5234 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 @@ -38,7 +38,6 @@ data class DemoScenario( val libxposedApiVersion: Int = -1, val frameworkVersionCode: Long = -1, val rootImplementation: Int = IManagerService.ROOT_MAGISK, - val rootVersion: String? = "28.1", val install: InstallScript = InstallScript.SUCCEEDS, /** @@ -156,7 +155,6 @@ val DEMO_SCENARIOS: List = title = "No root implementation", summary = "Nothing to flash through. The install path must refuse, not fail.", rootImplementation = IManagerService.ROOT_NONE, - rootVersion = null, install = DemoScenario.InstallScript.NO_ROOT, ), DemoScenario( @@ -164,7 +162,6 @@ val DEMO_SCENARIOS: List = title = "Two root implementations fighting", summary = "Flashing through either would be a guess, and must be named as such.", rootImplementation = IManagerService.ROOT_MULTIPLE, - rootVersion = null, install = DemoScenario.InstallScript.NO_ROOT, ), DemoScenario( @@ -172,7 +169,6 @@ val DEMO_SCENARIOS: List = title = "Root implementation too old", summary = "Installed but not usable. Distinct from having none.", rootImplementation = IManagerService.ROOT_TOO_OLD, - rootVersion = "20.4", install = DemoScenario.InstallScript.NO_ROOT, ), DemoScenario( @@ -180,14 +176,12 @@ val DEMO_SCENARIOS: List = title = "KernelSU", summary = "The install path quotes the implementation it found.", rootImplementation = IManagerService.ROOT_KERNELSU, - rootVersion = "12045", ), DemoScenario( id = "root-apatch", title = "APatch", summary = "As above, third implementation.", rootImplementation = IManagerService.ROOT_APATCH, - rootVersion = "10763", ), DemoScenario( id = "update-available", 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 14dde3051..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 @@ -104,7 +104,6 @@ class FakeManagerService( */ override fun getBuildStamp(): String? = real?.buildStamp - override fun getRootImplementationVersion(): String? = scenario.rootVersion /** * A flash, without a flash. 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 a547b4f42..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 @@ -349,10 +349,6 @@ class DaemonClient(private val serviceState: StateFlow) { suspend fun getRootImplementation(): Result = runIpc { it.rootImplementation } - suspend fun getRootImplementationVersion(): Result = runIpc { - it.rootImplementationVersion - } - /** * Starts a flash and returns as soon as the daemon has accepted it. * diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt index db14e8c33..98411ade7 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt @@ -21,7 +21,7 @@ import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW /** Which root implementation is in charge, and whether it can be flashed through. */ -data class RootState(val code: Int = IManagerService.ROOT_UNKNOWN, val version: String? = null) { +data class RootState(val code: Int = IManagerService.ROOT_UNKNOWN) { // Named implementations only. ROOT_UNKNOWN is also what a binder proxy returns for a // transaction the daemon does not implement, so it has to refuse rather than guess at an @@ -175,16 +175,15 @@ class FrameworkUpdateViewModel : ViewModel() { init { viewModelScope.launch { - // Two logs for the four requests these blocks make. They all fail from the same - // unreachable binder, so only the two that decide what the screen says are recorded; - // the root version and the build stamp take their default in silence. + // One log for the requests these blocks make. They all fail from the same unreachable + // binder, so only the one that decides what the screen says is recorded; the build + // stamp takes its default in silence. val code = daemon.getRootImplementation().getOrElse { e -> logW("update: root implementation unreadable, screen will say it is unknown", e) IManagerService.ROOT_UNKNOWN } - val version = daemon.getRootImplementationVersion().getOrNull() - _root.value = RootState(code, version) + _root.value = RootState(code) } viewModelScope.launch { val installed = 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 index 3760f80a3..674f50f38 100644 --- 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 @@ -682,17 +682,6 @@ interface IManagerService { */ int getRootImplementation(); - /** - * What the root implementation calls itself, for the manager to quote - {@code Magisk 27.0}, - * {@code KernelSU (64e3761d)}, {@code APatch 10762}. - * - *

Not always one version: for {@link #ROOT_MULTIPLE} it is every implementation that - * answered, comma-joined, since the point of that state is that there is more than one.

- * - * @return null when nothing was detected, or when something was detected and would not say - */ - @nullable String getRootImplementationVersion(); - /** * Flashes the framework's own root-module zip through whatever root implementation is managing * the device. From 6057399d6e7af582dae2e502c43f698bcc1896a7 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 05:23:40 +0200 Subject: [PATCH 12/13] Say the log rotation could not reach the daemon, rather than that it was refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `logs_rotate_failed` read "The daemon refused to start a new log", and there is no path by which the daemon refuses. `startNewLogPart` answers nothing — it writes a sentinel into the log for the native reader to act on afterwards — so the only failure the manager can observe is that the transaction never arrived: a dead binder, a descriptor that does not match, or no daemon at all. The inaccuracy predates this branch. `clearLogs` returned a boolean that was the constant `true`, so its false branch already meant "the transaction did not arrive" while the string said the daemon had refused. It sent a reader looking for a permission or a configuration problem when the framework was simply not reachable. A new key rather than a reworded one, because the meaning changes and eighteen translations would otherwise go on asserting the old one. Each translation keeps the word for the daemon that its own file already used — service, background process, 守护进程 — so only the claim changes. --- .../org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt | 2 +- manager/src/main/res/values-ar/strings_logs.xml | 2 +- manager/src/main/res/values-de/strings_logs.xml | 2 +- manager/src/main/res/values-es/strings_logs.xml | 2 +- manager/src/main/res/values-fa/strings_logs.xml | 2 +- manager/src/main/res/values-fr/strings_logs.xml | 2 +- manager/src/main/res/values-in/strings_logs.xml | 2 +- manager/src/main/res/values-it/strings_logs.xml | 2 +- manager/src/main/res/values-iw/strings_logs.xml | 2 +- manager/src/main/res/values-ja/strings_logs.xml | 2 +- manager/src/main/res/values-ko/strings_logs.xml | 2 +- manager/src/main/res/values-pl/strings_logs.xml | 2 +- manager/src/main/res/values-pt-rBR/strings_logs.xml | 2 +- manager/src/main/res/values-ru/strings_logs.xml | 2 +- manager/src/main/res/values-tr/strings_logs.xml | 2 +- manager/src/main/res/values-uk/strings_logs.xml | 2 +- manager/src/main/res/values-vi/strings_logs.xml | 2 +- manager/src/main/res/values-zh-rCN/strings_logs.xml | 2 +- manager/src/main/res/values-zh-rTW/strings_logs.xml | 2 +- manager/src/main/res/values/strings_logs.xml | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt index 5d630b7fd..7e352792b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt @@ -266,7 +266,7 @@ fun LogsScreen( if (confirmRotate) { val rotated = stringResource(R.string.logs_rotate_done) - val rotateFailed = stringResource(R.string.logs_rotate_failed) + val rotateFailed = stringResource(R.string.logs_rotate_unreachable) VectorAlertDialog( onDismissRequest = { confirmRotate = false }, title = { Text(stringResource(R.string.logs_rotate_title)) }, diff --git a/manager/src/main/res/values-ar/strings_logs.xml b/manager/src/main/res/values-ar/strings_logs.xml index 331be0324..2f4b1da04 100644 --- a/manager/src/main/res/values-ar/strings_logs.xml +++ b/manager/src/main/res/values-ar/strings_logs.xml @@ -66,7 +66,7 @@ لا يُحذف شيء. تُغلق الخدمة الجزء الحالي وتبدأ جزءًا جديدًا؛ ويبقى الجزء المغلق على القرص — على بعد سحبة — حتى يخرج من العشرة الأحدث، ويظل البلاغ المحفوظ يتضمّنه. بدء سجل جديد بدأ سجل جديد - رفضت الخدمة بدء سجل جديد + تعذّر الوصول إلى الخدمة لبدء سجل جديد إلغاء جارٍ جمع السجلات وملفات tombstone وdmesg… diff --git a/manager/src/main/res/values-de/strings_logs.xml b/manager/src/main/res/values-de/strings_logs.xml index 867975c66..085dba8ae 100644 --- a/manager/src/main/res/values-de/strings_logs.xml +++ b/manager/src/main/res/values-de/strings_logs.xml @@ -54,7 +54,7 @@ Es wird nichts gelöscht. Der Daemon schließt den aktuellen Teil und beginnt einen neuen; der geschlossene Teil bleibt auf dem Speicher — eine Wischgeste entfernt — bis er aus den zehn neuesten herausfällt, und ein gespeicherter Fehlerbericht enthält ihn weiterhin. Neues Protokoll beginnen Ein neues Protokoll wurde begonnen - Der Daemon hat es abgelehnt, ein neues Protokoll zu beginnen + Der Daemon war nicht erreichbar, um ein neues Protokoll zu beginnen Abbrechen Protokolle, Tombstones und dmesg werden gesammelt… diff --git a/manager/src/main/res/values-es/strings_logs.xml b/manager/src/main/res/values-es/strings_logs.xml index 949dc3114..efb726192 100644 --- a/manager/src/main/res/values-es/strings_logs.xml +++ b/manager/src/main/res/values-es/strings_logs.xml @@ -54,7 +54,7 @@ No se borra nada. El daemon cierra la parte actual y abre una nueva; la parte cerrada se queda en el disco —a un deslizamiento de aquí— hasta que salga de las diez más recientes, y un informe de error guardado la sigue incluyendo. Empezar un registro nuevo Se ha empezado un registro nuevo - El daemon se negó a empezar un registro nuevo + No se pudo contactar con el daemon para empezar un registro nuevo Cancelar Recogiendo registros, tombstones y dmesg… diff --git a/manager/src/main/res/values-fa/strings_logs.xml b/manager/src/main/res/values-fa/strings_logs.xml index 8fcd99b72..55661478f 100644 --- a/manager/src/main/res/values-fa/strings_logs.xml +++ b/manager/src/main/res/values-fa/strings_logs.xml @@ -54,7 +54,7 @@ چیزی حذف نمی‌شود. سرویس بخش کنونی را می‌بندد و بخشی تازه می‌گشاید؛ بخش بسته‌شده — به فاصلهٔ یک کشیدن — روی حافظه می‌ماند تا از ده بخش تازه بیرون برود، و گزارش اشکال ذخیره‌شده همچنان آن را دربر می‌گیرد. آغاز گزارش تازه گزارش تازه‌ای آغاز شد - سرویس از آغاز گزارش تازه سر باز زد + دسترسی به سرویس برای آغاز گزارش تازه ممکن نشد انصراف در حال گردآوری گزارش‌ها، tombstone و dmesg… diff --git a/manager/src/main/res/values-fr/strings_logs.xml b/manager/src/main/res/values-fr/strings_logs.xml index 58fa34fd2..2abc1f56c 100644 --- a/manager/src/main/res/values-fr/strings_logs.xml +++ b/manager/src/main/res/values-fr/strings_logs.xml @@ -54,7 +54,7 @@ Rien n\'est supprimé. Le démon ferme la partie en cours et en ouvre une neuve ; la partie fermée reste sur le disque — à un glissement d\'ici — jusqu\'à sortir des dix plus récentes, et un rapport de bug enregistré la contient toujours. Commencer un nouveau journal Un nouveau journal a été commencé - Le démon a refusé de commencer un nouveau journal + Impossible de joindre le démon pour commencer un nouveau journal Annuler Collecte des journaux, des tombstones et de dmesg… diff --git a/manager/src/main/res/values-in/strings_logs.xml b/manager/src/main/res/values-in/strings_logs.xml index d42133eb0..1ff9021ab 100644 --- a/manager/src/main/res/values-in/strings_logs.xml +++ b/manager/src/main/res/values-in/strings_logs.xml @@ -51,7 +51,7 @@ Tidak ada yang dihapus. Daemon menutup bagian yang sekarang dan membuka yang baru; bagian yang ditutup tetap ada di penyimpanan — sejauh satu usapan — sampai ia keluar dari sepuluh yang terbaru, dan laporan bug yang tersimpan tetap menyertakannya. Mulai log baru Log baru telah dimulai - Daemon menolak memulai log baru + Tidak dapat menghubungi daemon untuk memulai log baru Batal Mengumpulkan log, tombstone, dan dmesg… diff --git a/manager/src/main/res/values-it/strings_logs.xml b/manager/src/main/res/values-it/strings_logs.xml index 3f6ff977c..a6cdcd9ef 100644 --- a/manager/src/main/res/values-it/strings_logs.xml +++ b/manager/src/main/res/values-it/strings_logs.xml @@ -54,7 +54,7 @@ Non viene cancellato niente. Il daemon chiude la parte attuale e ne apre una nuova; la parte chiusa resta su disco — a uno scorrimento da qui — finché non esce dalle dieci più recenti, e una segnalazione di bug salvata la include ancora. Comincia un nuovo log È stato cominciato un nuovo log - Il daemon si è rifiutato di cominciare un nuovo log + Impossibile raggiungere il daemon per cominciare un nuovo log Annulla Raccolta di log, tombstone e dmesg… diff --git a/manager/src/main/res/values-iw/strings_logs.xml b/manager/src/main/res/values-iw/strings_logs.xml index 54ef2ba26..58a2eb10a 100644 --- a/manager/src/main/res/values-iw/strings_logs.xml +++ b/manager/src/main/res/values-iw/strings_logs.xml @@ -60,7 +60,7 @@ שום דבר לא נמחק. השירות סוגר את החלק הנוכחי ופותח חדש; החלק הסגור נשאר באחסון — במרחק החלקה אחת — עד שהוא יוצא מעשרת האחרונים, ודיווח תקלה שנשמר עדיין כולל אותו. התחלת יומן חדש התחיל יומן חדש - השירות סירב להתחיל יומן חדש + לא ניתן היה להגיע לשירות כדי להתחיל יומן חדש ביטול אוסף יומנים, קובצי tombstone ו-dmesg… diff --git a/manager/src/main/res/values-ja/strings_logs.xml b/manager/src/main/res/values-ja/strings_logs.xml index b3bb02d4c..e28324aba 100644 --- a/manager/src/main/res/values-ja/strings_logs.xml +++ b/manager/src/main/res/values-ja/strings_logs.xml @@ -51,7 +51,7 @@ 何も削除されません。デーモンは現在の区切りを閉じて新しいものを始めます。閉じた区切りは、直近 10 件から外れるまでは端末に残り — スワイプで見られます — 保存した不具合報告にも引き続き含まれます。 新しいログを始める 新しいログを始めました - デーモンが新しいログの開始を拒否しました + デーモンに接続できず、新しいログを開始できませんでした キャンセル ログ・tombstone・dmesg を集めています… diff --git a/manager/src/main/res/values-ko/strings_logs.xml b/manager/src/main/res/values-ko/strings_logs.xml index bc78f45be..9eaa4fdb5 100644 --- a/manager/src/main/res/values-ko/strings_logs.xml +++ b/manager/src/main/res/values-ko/strings_logs.xml @@ -51,7 +51,7 @@ 아무것도 지워지지 않습니다. 데몬이 지금 조각을 닫고 새 조각을 엽니다. 닫힌 조각은 최근 열 개에서 밀려날 때까지 기기에 남고 — 밀어서 볼 수 있습니다 — 저장한 버그 신고에도 그대로 들어갑니다. 새 로그 시작 새 로그를 시작했습니다 - 데몬이 새 로그 시작을 거부했습니다 + 데몬에 연결할 수 없어 새 로그를 시작하지 못했습니다 취소 로그, tombstone, dmesg를 모으는 중… diff --git a/manager/src/main/res/values-pl/strings_logs.xml b/manager/src/main/res/values-pl/strings_logs.xml index 14defcb4e..ca291f3b6 100644 --- a/manager/src/main/res/values-pl/strings_logs.xml +++ b/manager/src/main/res/values-pl/strings_logs.xml @@ -60,7 +60,7 @@ Nic nie zostanie usunięte. Usługa zamyka bieżącą część i otwiera nową; zamknięta część zostaje na dysku — o jedno przesunięcie stąd — dopóki nie wypadnie z dziesięciu najnowszych, a zapisane zgłoszenie błędu nadal ją zawiera. Zacznij nowy dziennik Rozpoczęto nowy dziennik - Usługa odmówiła rozpoczęcia nowego dziennika + Nie udało się połączyć z usługą, aby rozpocząć nowy dziennik Anuluj Zbieranie dzienników, tombstone\'ów i dmesg… diff --git a/manager/src/main/res/values-pt-rBR/strings_logs.xml b/manager/src/main/res/values-pt-rBR/strings_logs.xml index 14a6ebd8c..6fd03724a 100644 --- a/manager/src/main/res/values-pt-rBR/strings_logs.xml +++ b/manager/src/main/res/values-pt-rBR/strings_logs.xml @@ -54,7 +54,7 @@ Nada é apagado. O daemon fecha a parte atual e abre uma nova; a parte fechada continua no disco — a um deslize daqui — até sair das dez mais recentes, e um relatório de erro salvo ainda a inclui. Começar um registro novo Um registro novo foi começado - O daemon se recusou a começar um registro novo + Não foi possível contatar o daemon para começar um registro novo Cancelar Reunindo registros, tombstones e dmesg… diff --git a/manager/src/main/res/values-ru/strings_logs.xml b/manager/src/main/res/values-ru/strings_logs.xml index 029442ad8..dda662ae1 100644 --- a/manager/src/main/res/values-ru/strings_logs.xml +++ b/manager/src/main/res/values-ru/strings_logs.xml @@ -51,7 +51,7 @@ Ничего не удаляется. Служба закрывает текущую часть и начинает новую; закрытая часть остаётся на диске — в одном свайпе отсюда — пока не выйдет за пределы последних десяти, и попадает в сохранённый отчёт об ошибке. Начать новый журнал Новый журнал начат - Служба отказалась начать новый журнал + Не удалось связаться со службой, чтобы начать новый журнал Отмена Сбор журналов, tombstone и dmesg… Отчёт об ошибке сохранён diff --git a/manager/src/main/res/values-tr/strings_logs.xml b/manager/src/main/res/values-tr/strings_logs.xml index acbe87a15..a2a6bb245 100644 --- a/manager/src/main/res/values-tr/strings_logs.xml +++ b/manager/src/main/res/values-tr/strings_logs.xml @@ -54,7 +54,7 @@ Hiçbir şey silinmez. Art alan süreci mevcut parçayı kapatıp yenisini açar; kapanan parça, en yeni on parçanın dışına düşene dek diskte kalır — bir kaydırma kadar uzakta — ve kaydedilen hata raporuna da girmeye devam eder. Yeni bir günlük başlat Yeni bir günlük başlatıldı - Art alan süreci yeni günlük başlatmayı reddetti + Yeni günlük başlatmak için art alan sürecine ulaşılamadı Vazgeç Günlükler, tombstone\'lar ve dmesg toplanıyor… diff --git a/manager/src/main/res/values-uk/strings_logs.xml b/manager/src/main/res/values-uk/strings_logs.xml index 604d37cf6..8bc9c0a0b 100644 --- a/manager/src/main/res/values-uk/strings_logs.xml +++ b/manager/src/main/res/values-uk/strings_logs.xml @@ -60,7 +60,7 @@ Нічого не видаляється. Служба закриває поточну частину й починає нову; закрита частина лишається на диску — за один змах звідси — доки не випаде з десяти найновіших, і збережений звіт про помилку все одно її містить. Почати новий журнал Розпочато новий журнал - Служба відмовилася починати новий журнал + Не вдалося звернутися до служби, щоб почати новий журнал Скасувати Збирання журналів, tombstone і dmesg… diff --git a/manager/src/main/res/values-vi/strings_logs.xml b/manager/src/main/res/values-vi/strings_logs.xml index 40f85239e..091880c9b 100644 --- a/manager/src/main/res/values-vi/strings_logs.xml +++ b/manager/src/main/res/values-vi/strings_logs.xml @@ -51,7 +51,7 @@ Không xoá gì cả. Tiến trình nền đóng phần hiện tại và mở một phần mới; phần đã đóng vẫn nằm trên bộ nhớ — chỉ một cái vuốt — cho tới khi rơi khỏi mười phần gần nhất, và báo cáo lỗi đã lưu vẫn có nó. Bắt đầu nhật ký mới Đã bắt đầu một nhật ký mới - Tiến trình nền từ chối bắt đầu nhật ký mới + Không thể kết nối tiến trình nền để bắt đầu nhật ký mới Huỷ Đang thu thập nhật ký, tombstone và dmesg… diff --git a/manager/src/main/res/values-zh-rCN/strings_logs.xml b/manager/src/main/res/values-zh-rCN/strings_logs.xml index 5fb01539d..0ecba3e47 100644 --- a/manager/src/main/res/values-zh-rCN/strings_logs.xml +++ b/manager/src/main/res/values-zh-rCN/strings_logs.xml @@ -51,7 +51,7 @@ 不会删除任何内容。守护进程会关闭当前分段并新建一个;已关闭的分段仍保留在磁盘上——滑动即可查看——直到它不再属于最近的十个分段,保存的问题报告也仍会包含它。 开始新的日志 已开始新的日志 - 守护进程拒绝开始新的日志 + 无法连接守护进程以开始新的日志 取消 正在收集日志、tombstone 和 dmesg… diff --git a/manager/src/main/res/values-zh-rTW/strings_logs.xml b/manager/src/main/res/values-zh-rTW/strings_logs.xml index e24218441..b165975f3 100644 --- a/manager/src/main/res/values-zh-rTW/strings_logs.xml +++ b/manager/src/main/res/values-zh-rTW/strings_logs.xml @@ -51,7 +51,7 @@ 不會刪除任何內容。常駐程式會關閉目前的分段並新建一個;已關閉的分段仍留在磁碟上——滑動即可檢視——直到它不再屬於最近的十個分段,儲存的問題報告也仍會包含它。 開始新的日誌 已開始新的日誌 - 常駐程式拒絕開始新的日誌 + 無法連線常駐程式以開始新的日誌 取消 正在收集日誌、tombstone 與 dmesg… diff --git a/manager/src/main/res/values/strings_logs.xml b/manager/src/main/res/values/strings_logs.xml index 451fe83ff..5b3c606b9 100644 --- a/manager/src/main/res/values/strings_logs.xml +++ b/manager/src/main/res/values/strings_logs.xml @@ -71,7 +71,7 @@ Nothing is deleted. The daemon closes the current part and starts a fresh one; the closed part stays on disk — a swipe away — until it ages out of the ten most recent, and a saved bug report still includes it. Start a new log A new log has been started - The daemon refused to start a new log + Could not reach the daemon to start a new log Cancel From 33e0496336a4cb9133b05f64c20a75c1fa21f8fa Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 4 Aug 2026 09:22:06 +0200 Subject: [PATCH 13/13] Stop deciding whether a root implementation is too old to flash through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon carried NeoZygisk's version floors — 26402 for Magisk, 10762 for APatch — and answered ROOT_TOO_OLD below them, which the update screen rendered as "too old to flash through". That was never the daemon's decision to make. Whether the zygisk loader runs on this device is settled before the daemon exists; a daemon that is answering at all has already passed whatever check the loader applies. Duplicating the floor here only created a second opinion that could disagree with the first, and it had to be kept in step with a number living in another project. The KernelSU branch already said as much: it applies no floor, because `ksud -V` prints a build hash and there is nothing to compare — and that was sound, not a gap. So detection reports what it found and quotes the version back for the user to read. ROOT_TOO_OLD goes, with the string and the demo scenario that rendered it, and the remaining ROOT_* values close up behind it. ROOT_UNKNOWN keeps 0, which it holds because 0 is what a binder proxy returns for a transaction the daemon does not implement. The prose loses its running commentary on NeoZygisk's internals along the way. What is worth saying is why detection goes by binary — the binary has to exist to do the flashing anyway — not how a different project reaches the same answer. --- .../vector/daemon/utils/RootImplementation.kt | 60 ++++--------------- .../vector/manager/demo/DemoScenario.kt | 7 --- .../update/FrameworkUpdateViewModel.kt | 8 +-- manager/src/main/res/values-ar/strings.xml | 1 - manager/src/main/res/values-de/strings.xml | 1 - manager/src/main/res/values-es/strings.xml | 1 - manager/src/main/res/values-fa/strings.xml | 1 - manager/src/main/res/values-fr/strings.xml | 1 - manager/src/main/res/values-in/strings.xml | 1 - manager/src/main/res/values-it/strings.xml | 1 - manager/src/main/res/values-iw/strings.xml | 1 - manager/src/main/res/values-ja/strings.xml | 1 - manager/src/main/res/values-ko/strings.xml | 1 - manager/src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - manager/src/main/res/values-ru/strings.xml | 1 - manager/src/main/res/values-tr/strings.xml | 1 - manager/src/main/res/values-uk/strings.xml | 1 - manager/src/main/res/values-vi/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - .../src/main/res/values-zh-rTW/strings.xml | 1 - manager/src/main/res/values/strings.xml | 1 - .../matrix/vector/ipc/IManagerService.aidl | 46 ++++---------- 23 files changed, 27 insertions(+), 113 deletions(-) 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 8b964eecf..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 @@ -12,32 +12,18 @@ 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. * @@ -88,27 +74,12 @@ 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) IManagerService.ROOT_MAGISK else IManagerService.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 @@ -117,22 +88,15 @@ object RootImplementation { } /** - * 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(IManagerService.ROOT_APATCH, "APatch ($output)", binary) - code >= MIN_APATCH -> Detection(IManagerService.ROOT_APATCH, "APatch $code", binary) - else -> Detection(IManagerService.ROOT_TOO_OLD, "APatch $code", binary) - } + return Detection(IManagerService.ROOT_APATCH, "APatch ${code ?: "($output)"}", binary) } /** 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 d9b7e5234..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 @@ -164,13 +164,6 @@ val DEMO_SCENARIOS: List = rootImplementation = IManagerService.ROOT_MULTIPLE, 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 = IManagerService.ROOT_TOO_OLD, - install = DemoScenario.InstallScript.NO_ROOT, - ), DemoScenario( id = "root-ksu", title = "KernelSU", diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt index 98411ade7..888988f6f 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateViewModel.kt @@ -35,18 +35,14 @@ data class RootState(val code: Int = IManagerService.ROOT_UNKNOWN) { /** * The sentence to show when flashing is not possible. * - * Null when it is, and four different strings when it is not — "nothing installed", "too old", - * "two of them" and "this daemon does not say" need four different actions from the reader, and + * Null when it is, and three different strings when it is not — "nothing installed", "two of + * them" and "this daemon does not say" need three different actions from the reader, and * collapsing them into one "unsupported" would tell someone with two root managers to go * install a root manager. */ @androidx.compose.runtime.Composable fun label(): String? = when (code) { - IManagerService.ROOT_TOO_OLD -> - androidx.compose.ui.res.stringResource( - org.matrix.vector.manager.R.string.update_root_too_old - ) IManagerService.ROOT_MULTIPLE -> androidx.compose.ui.res.stringResource( org.matrix.vector.manager.R.string.update_root_multiple diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 78681d8c1..aec8f412a 100644 --- a/manager/src/main/res/values-ar/strings.xml +++ b/manager/src/main/res/values-ar/strings.xml @@ -323,7 +323,6 @@ لا يوجد تحديث متاح. لم يَنشر هذا الإصدار أي ملاحظات. لم يُعثر على أي تطبيق للروت، فلا شيء يمكن التثبيت من خلاله. - تطبيق الروت على هذا الجهاز أقدم من أن يُثبَّت من خلاله. عُثر على تطبيقَي روت. لن يخمّن Vector أيهما يدير الوحدة. تعذّر بدء برنامج التثبيت. تعذّر تنزيل التحديث. diff --git a/manager/src/main/res/values-de/strings.xml b/manager/src/main/res/values-de/strings.xml index e349e78d2..9fadbe0a1 100644 --- a/manager/src/main/res/values-de/strings.xml +++ b/manager/src/main/res/values-de/strings.xml @@ -283,7 +283,6 @@ Kein Update verfügbar. Zu dieser Version wurden keine Notizen veröffentlicht. Keine Root-Implementierung gefunden — es lässt sich nichts flashen. - Die Root-Implementierung auf diesem Gerät ist zu alt zum Flashen. Zwei Root-Implementierungen gefunden. Vector rät nicht, welche das Modul verwaltet. Das Installationsprogramm ließ sich nicht starten. Das Update konnte nicht geladen werden. diff --git a/manager/src/main/res/values-es/strings.xml b/manager/src/main/res/values-es/strings.xml index fbb299ee0..c280fdde5 100644 --- a/manager/src/main/res/values-es/strings.xml +++ b/manager/src/main/res/values-es/strings.xml @@ -283,7 +283,6 @@ No hay ninguna actualización. Esta versión no publicó notas. No se encontró ninguna implementación de root, así que no se puede flashear nada. - La implementación de root de este dispositivo es demasiado antigua para flashear. Se encontraron dos implementaciones de root. Vector no va a adivinar cuál gestiona el módulo. No se pudo iniciar el instalador. No se pudo descargar la actualización. diff --git a/manager/src/main/res/values-fa/strings.xml b/manager/src/main/res/values-fa/strings.xml index b1bb3d0ce..42a5562ef 100644 --- a/manager/src/main/res/values-fa/strings.xml +++ b/manager/src/main/res/values-fa/strings.xml @@ -283,7 +283,6 @@ به‌روزرسانی‌ای در دسترس نیست. این انتشار یادداشتی منتشر نکرده است. هیچ پیاده‌سازی روتی یافت نشد، پس چیزی نمی‌توان نصب کرد. - پیاده‌سازی روت روی این دستگاه برای نصب بسیار قدیمی است. دو پیاده‌سازی روت یافت شد. Vector حدس نمی‌زند کدام‌یک ماژول را مدیریت می‌کند. نصب‌کننده اجرا نشد. به‌روزرسانی بارگیری نشد. diff --git a/manager/src/main/res/values-fr/strings.xml b/manager/src/main/res/values-fr/strings.xml index 8b831df17..0f2ea0c8f 100644 --- a/manager/src/main/res/values-fr/strings.xml +++ b/manager/src/main/res/values-fr/strings.xml @@ -283,7 +283,6 @@ Aucune mise à jour disponible. Cette version n\'a pas publié de notes. Aucune implémentation root trouvée : rien ne peut être flashé. - L\'implémentation root de cet appareil est trop ancienne pour flasher. Deux implémentations root trouvées. Vector ne devinera pas laquelle gère le module. Impossible de lancer l\'installateur. Impossible de télécharger la mise à jour. diff --git a/manager/src/main/res/values-in/strings.xml b/manager/src/main/res/values-in/strings.xml index dd95b761a..3b32dabc3 100644 --- a/manager/src/main/res/values-in/strings.xml +++ b/manager/src/main/res/values-in/strings.xml @@ -277,7 +277,6 @@ Tidak ada pembaruan. Rilis ini tidak menerbitkan catatan. Tidak ditemukan implementasi root, jadi tidak ada yang bisa dipasang. - Implementasi root di perangkat ini terlalu lama untuk dipakai memasang. Ditemukan dua implementasi root. Vector tidak akan menebak yang mana yang mengurus modulnya. Pemasang tidak bisa dijalankan. Pembaruan tidak bisa diunduh. diff --git a/manager/src/main/res/values-it/strings.xml b/manager/src/main/res/values-it/strings.xml index 9acc62eba..b14a6a1e2 100644 --- a/manager/src/main/res/values-it/strings.xml +++ b/manager/src/main/res/values-it/strings.xml @@ -283,7 +283,6 @@ Nessun aggiornamento disponibile. Questa versione non ha pubblicato note. Non è stata trovata nessuna implementazione di root, quindi non c\'è niente su cui installare. - L\'implementazione di root su questo dispositivo è troppo vecchia per installarci sopra. Sono state trovate due implementazioni di root. Vector non tira a indovinare quale gestisca il modulo. Non è stato possibile avviare il programma di installazione. Non è stato possibile scaricare l\'aggiornamento. diff --git a/manager/src/main/res/values-iw/strings.xml b/manager/src/main/res/values-iw/strings.xml index 3a7a38d48..c7f45c39b 100644 --- a/manager/src/main/res/values-iw/strings.xml +++ b/manager/src/main/res/values-iw/strings.xml @@ -307,7 +307,6 @@ אין עדכון זמין. הגרסה הזאת לא פרסמה הערות. לא נמצא שום מימוש הרשאות root, ולכן אין דרך להתקין. - מימוש ה-root במכשיר הזה ישן מכדי להתקין דרכו. נמצאו שני מימושי root. ‏Vector לא ינחש איזה מהם מנהל את המודול. לא ניתן היה להפעיל את תוכנית ההתקנה. לא ניתן היה להוריד את העדכון. diff --git a/manager/src/main/res/values-ja/strings.xml b/manager/src/main/res/values-ja/strings.xml index a0543c0ba..20dd15841 100644 --- a/manager/src/main/res/values-ja/strings.xml +++ b/manager/src/main/res/values-ja/strings.xml @@ -273,7 +273,6 @@ 利用できる更新はありません。 このリリースには説明がありません。 root 実装が見つからないため、書き込めません。 - この端末の root 実装は古すぎて書き込みに使えません。 root 実装が 2 つ見つかりました。どちらがモジュールを管理しているか、Vector は推測しません。 インストーラーを起動できませんでした。 更新をダウンロードできませんでした。 diff --git a/manager/src/main/res/values-ko/strings.xml b/manager/src/main/res/values-ko/strings.xml index c1d79d578..508dd8728 100644 --- a/manager/src/main/res/values-ko/strings.xml +++ b/manager/src/main/res/values-ko/strings.xml @@ -273,7 +273,6 @@ 사용할 수 있는 업데이트가 없습니다. 이 릴리스에는 설명이 없습니다. root 구현을 찾지 못해 설치할 수 없습니다. - 이 기기의 root 구현이 너무 오래되어 설치에 쓸 수 없습니다. root 구현이 두 개 발견되었습니다. 어느 쪽이 모듈을 관리하는지 Vector가 짐작하지 않습니다. 설치 프로그램을 시작하지 못했습니다. 업데이트를 내려받지 못했습니다. diff --git a/manager/src/main/res/values-pl/strings.xml b/manager/src/main/res/values-pl/strings.xml index ca419df13..213588e53 100644 --- a/manager/src/main/res/values-pl/strings.xml +++ b/manager/src/main/res/values-pl/strings.xml @@ -303,7 +303,6 @@ Brak dostępnych aktualizacji. To wydanie nie opublikowało informacji. Nie znaleziono żadnej implementacji roota, więc nie ma czym wgrać. - Implementacja roota na tym urządzeniu jest za stara, aby przez nią wgrywać. Znaleziono dwie implementacje roota. Vector nie będzie zgadywał, która zarządza modułem. Nie udało się uruchomić instalatora. Nie udało się pobrać aktualizacji. diff --git a/manager/src/main/res/values-pt-rBR/strings.xml b/manager/src/main/res/values-pt-rBR/strings.xml index be30ea165..90539e5a5 100644 --- a/manager/src/main/res/values-pt-rBR/strings.xml +++ b/manager/src/main/res/values-pt-rBR/strings.xml @@ -283,7 +283,6 @@ Nenhuma atualização disponível. Esta versão não publicou notas. Nenhuma implementação de root foi encontrada, então não há como gravar nada. - A implementação de root deste dispositivo é antiga demais para gravar. Foram encontradas duas implementações de root. O Vector não vai adivinhar qual delas cuida do módulo. Não foi possível iniciar o instalador. Não foi possível baixar a atualização. diff --git a/manager/src/main/res/values-ru/strings.xml b/manager/src/main/res/values-ru/strings.xml index 97b963393..085b996e6 100644 --- a/manager/src/main/res/values-ru/strings.xml +++ b/manager/src/main/res/values-ru/strings.xml @@ -285,7 +285,6 @@ Обновлений нет. К этому выпуску нет описания. Не найдено ни одной реализации root — прошить нечем. - Реализация root на этом устройстве слишком старая для прошивки. Найдены две реализации root. Vector не станет гадать, какая из них управляет модулем. Не удалось запустить установщик. Не удалось загрузить обновление. diff --git a/manager/src/main/res/values-tr/strings.xml b/manager/src/main/res/values-tr/strings.xml index 0f0f1856b..22bf4aaa2 100644 --- a/manager/src/main/res/values-tr/strings.xml +++ b/manager/src/main/res/values-tr/strings.xml @@ -283,7 +283,6 @@ Kullanılabilir güncelleme yok. Bu sürüm için not yayımlanmamış. Hiçbir root uygulaması bulunamadı, dolayısıyla kurulacak bir şey yok. - Bu cihazdaki root uygulaması kurulum için fazla eski. İki root uygulaması bulundu. Vector modülü hangisinin yönettiğini tahmin etmez. Kurulum programı başlatılamadı. Güncelleme indirilemedi. diff --git a/manager/src/main/res/values-uk/strings.xml b/manager/src/main/res/values-uk/strings.xml index 1bd30571b..f016134bc 100644 --- a/manager/src/main/res/values-uk/strings.xml +++ b/manager/src/main/res/values-uk/strings.xml @@ -303,7 +303,6 @@ Оновлень немає. До цього випуску немає опису. Не знайдено жодної реалізації root, тож немає чим прошивати. - Реалізація root на цьому пристрої застара для прошивання. Знайдено дві реалізації root. Vector не гадатиме, яка з них керує модулем. Не вдалося запустити встановлювач. Не вдалося завантажити оновлення. diff --git a/manager/src/main/res/values-vi/strings.xml b/manager/src/main/res/values-vi/strings.xml index c8bcd0d27..3ba8e9337 100644 --- a/manager/src/main/res/values-vi/strings.xml +++ b/manager/src/main/res/values-vi/strings.xml @@ -273,7 +273,6 @@ Không có bản cập nhật nào. Bản phát hành này không có ghi chú. Không tìm thấy bản cài root nào nên không nạp được gì. - Bản cài root trên thiết bị này quá cũ để nạp. Tìm thấy hai bản cài root. Vector sẽ không đoán bản nào đang quản lý mô-đun. Không khởi chạy được trình cài đặt. Không tải được bản cập nhật. diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 5e34315ee..a8bcf2c60 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -274,7 +274,6 @@ 没有可用更新。 此版本未发布说明。 未找到 root 实现,无法刷入。 - 此设备上的 root 实现过旧,无法用于刷入。 检测到两种 root 实现。Vector 不会猜测由哪一个管理模块。 无法启动安装程序。 无法下载此更新。 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 0aadbdf24..da72e25cb 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -274,7 +274,6 @@ 沒有可用更新。 此版本未發布說明。 找不到 root 實作,無法刷入。 - 此裝置上的 root 實作過舊,無法用於刷入。 偵測到兩種 root 實作。Vector 不會猜測由哪一個管理模組。 無法啟動安裝程式。 無法下載此更新。 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 922caa631..7a1d4dc1b 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -430,7 +430,6 @@ No update available. This release published no notes. No root implementation was found, so nothing can be flashed. - The root implementation on this device is too old to flash through. This framework build does not report which root implementation is installed. Two root implementations were found. Vector will not guess which one owns the module. The installer could not be started. 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 index 674f50f38..19319ba12 100644 --- 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 @@ -675,10 +675,12 @@ interface IManagerService { /** * Which root implementation is managing this device, as one of the {@code ROOT_*} constants. * - *

Detected once and cached, because detecting it forks each candidate binary and reads its - * version. 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}.

+ *

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(); @@ -740,14 +742,6 @@ interface IManagerService { /** No root implementation was found, so nothing can be flashed. */ const int ROOT_NONE = 1; - /** - * One was found, below the version floor the zygisk loader requires. - * - *

Kept apart from {@link #ROOT_NONE} because the two need different sentences: one asks the - * reader to install a root manager, the other to update the one they have.

- */ - const int ROOT_TOO_OLD = 2; - /** * More than one was found. * @@ -755,28 +749,14 @@ interface IManagerService { * flashing through either would be guessing which one owns the module tree on the reader's * behalf.

*/ - const int ROOT_MULTIPLE = 3; + const int ROOT_MULTIPLE = 2; - /** Magisk, at or above the version floor the zygisk loader requires. */ - const int ROOT_MAGISK = 4; + /** Magisk. */ + const int ROOT_MAGISK = 3; - /** - * KernelSU. - * - *

No version floor is applied, and that is sound rather than a shrug. {@code ksud -V} prints - * a build hash rather than a version code, so there is nothing to compare - the version lives - * behind KernelSU's own prctl interface, which a shell cannot reach. Presence is therefore the - * whole test, and the check that cannot be made here has already been made one layer down: the - * zygisk loader refuses to load on a KernelSU below its floor, so a daemon that is running at - * all is running under one new enough.

- */ - const int ROOT_KERNELSU = 5; + /** KernelSU. */ + const int ROOT_KERNELSU = 4; - /** - * APatch, at or above the version floor the zygisk loader requires - or one whose version - * string this daemon could not parse, which is reported as present rather than as absent. - * Refusing to flash because our own parser did not recognise a version would be refusing on the - * evidence of our code rather than on the state of the device. - */ - const int ROOT_APATCH = 6; + /** APatch. */ + const int ROOT_APATCH = 5; }