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
+ *
+ * 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;
}