From b186e3361bd7f3b0b93eef761a1cdb174fef7880 Mon Sep 17 00:00:00 2001 From: NkBe Date: Sat, 4 Jul 2026 20:47:18 +0800 Subject: [PATCH] Complete API102 service integration --- .../daemon/ipc/InjectedModuleService.kt | 101 +++++++++++- .../matrix/vector/daemon/ipc/ModuleService.kt | 6 + .../service/ILSPInjectedModuleService.aidl | 15 ++ .../vector/impl/core/VectorModuleManager.kt | 32 ++++ .../vector/impl/core/VectorXposedService.kt | 147 ++++++++++++++++++ 5 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorXposedService.kt diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt index cff900d01..fefc9b08e 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt @@ -6,20 +6,25 @@ import android.os.ParcelFileDescriptor import android.os.RemoteException import android.util.Log import io.github.libxposed.service.IXposedService +import io.github.libxposed.service.HookedProcess +import io.github.libxposed.service.IHotReloadCallback +import io.github.libxposed.service.IXposedScopeCallback import java.io.Serializable import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.service.ILSPInjectedModuleService import org.lsposed.lspd.service.IRemotePreferenceCallback import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.ModuleDatabase import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.system.NotificationManager import org.matrix.vector.daemon.system.PER_USER_RANGE private const val TAG = "VectorInjectedModuleService" class InjectedModuleService(private val packageName: String) : ILSPInjectedModuleService.Stub() { - // Tracks active RemotePreferenceCallbacks linked by config group + // Preference instances in hooked processes subscribe here for daemon-side updates. private val callbacks = ConcurrentHashMap>() override fun getFrameworkProperties(): Long { @@ -30,6 +35,55 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul return prop } + override fun getScope(): List = + ConfigCache.getModuleScope(packageName)?.map { it.packageName } ?: emptyList() + + override fun requestScope(packages: List, callback: IXposedScopeCallback?) { + val userId = Binder.getCallingUid() / PER_USER_RANGE + if (PreferenceStore.isScopeRequestBlocked(packageName)) { + callback?.onScopeRequestFailed("Scope request blocked by user configuration") + return + } + packages.forEach { pkg -> + if (callback != null) NotificationManager.requestModuleScope(packageName, userId, pkg, callback) + } + } + + override fun removeScope(packages: List) { + val userId = Binder.getCallingUid() / PER_USER_RANGE + packages.forEach { pkg -> + runCatching { ModuleDatabase.removeModuleScope(packageName, pkg, userId) } + .getOrElse { throw RemoteException(it.message) } + } + } + + override fun getRunningTargets(): List { + val module = ConfigCache.getModuleByPackage(packageName) ?: return emptyList() + return ApplicationService.getRunningTargets(module) + } + + override fun hotReloadModule(targetId: Long, data: Bundle?, callback: IHotReloadCallback?) { + runCatching { + val module = ConfigCache.getModuleByPackage(packageName) + ?: throw HotReloadUnsupportedException("Module $packageName is not enabled") + if (module.file.moduleClassNames.size != 1) { + throw HotReloadUnsupportedException("Hot reload requires exactly one Java entry class") + } + ApplicationService.hotReloadTarget(targetId, module, data) + callback?.onHotReloadResult(IXposedService.HOT_RELOAD_SUCCEEDED, null) + } + .onFailure { throwable -> + if (throwable is SecurityException) throw throwable + val status = when (throwable) { + is HotReloadInProgressException -> IXposedService.HOT_RELOAD_IN_PROGRESS + is HotReloadProcessDiedException -> IXposedService.HOT_RELOAD_PROCESS_DIED + is HotReloadUnsupportedException -> IXposedService.HOT_RELOAD_UNSUPPORTED + else -> IXposedService.HOT_RELOAD_FAILED + } + callback?.onHotReloadResult(status, throwable.message) + } + } + override fun requestRemotePreferences( group: String, callback: IRemotePreferenceCallback? @@ -48,12 +102,41 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul return bundle } + @Suppress("DEPRECATION") + override fun updateRemotePreferences(group: String, diff: Bundle) { + val userId = Binder.getCallingUid() / PER_USER_RANGE + val values = mutableMapOf() + if (diff.getBoolean("clear", false)) { + PreferenceStore.deleteModulePrefs(packageName, userId, group) + } + (diff.getSerializable("delete") as? Set<*>)?.forEach { key -> + if (key is String) values[key] = null + } + (diff.getSerializable("put") as? Map<*, *>)?.forEach { (key, value) -> + if (key is String) values[key] = value + } + runCatching { + PreferenceStore.updateModulePrefs(packageName, userId, group, values) + onUpdateRemotePreferences(group, diff) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemotePreferences(group: String) { + val userId = Binder.getCallingUid() / PER_USER_RANGE + runCatching { PreferenceStore.deleteModulePrefs(packageName, userId, group) } + .getOrElse { throw RemoteException(it.message) } + onUpdateRemotePreferences(group, Bundle().apply { putBoolean("clear", true) }) + } + override fun openRemoteFile(path: String): ParcelFileDescriptor { FileSystem.ensureModuleFilePath(path) val userId = Binder.getCallingUid() / PER_USER_RANGE return runCatching { val dir = FileSystem.resolveModuleDir(packageName, "files", userId, -1) - ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + ParcelFileDescriptor.open( + dir.resolve(path).toFile(), + ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE) } .getOrElse { throw RemoteException(it.message) } } @@ -67,7 +150,19 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul .getOrElse { throw RemoteException(it.message) } } - // Called by ModuleService when prefs are updated globally + override fun deleteRemoteFile(path: String): Boolean { + FileSystem.ensureModuleFilePath(path) + val userId = Binder.getCallingUid() / PER_USER_RANGE + return runCatching { + FileSystem.resolveModuleDir(packageName, "files", userId, -1) + .resolve(path) + .toFile() + .delete() + } + .getOrElse { throw RemoteException(it.message) } + } + + // Keep hooked-process preference caches in sync with updates from the module app. fun onUpdateRemotePreferences(group: String, diff: Bundle) { val groupCallbacks = callbacks[group] ?: return for (callback in groupCallbacks) { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index 9c8b6eb4d..d79195035 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -195,6 +195,10 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { val userId = ensureModule() val values = mutableMapOf() + if (diff.getBoolean("clear", false)) { + PreferenceStore.deleteModulePrefs(loadedModule.packageName, userId, group) + } + diff.getSerializable("delete")?.let { deletes -> (deletes as Set<*>).forEach { values[it as String] = null } } @@ -211,6 +215,8 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { override fun deleteRemotePreferences(group: String) { PreferenceStore.deleteModulePrefs(loadedModule.packageName, ensureModule(), group) + (loadedModule.service as? InjectedModuleService) + ?.onUpdateRemotePreferences(group, Bundle().apply { putBoolean("clear", true) }) } override fun listRemoteFiles(): Array { diff --git a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl index c77ece391..01710a562 100644 --- a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl +++ b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl @@ -1,13 +1,28 @@ package org.lsposed.lspd.service; import org.lsposed.lspd.service.IRemotePreferenceCallback; +import io.github.libxposed.service.HookedProcess; +import io.github.libxposed.service.IHotReloadCallback; +import io.github.libxposed.service.IXposedScopeCallback; interface ILSPInjectedModuleService { long getFrameworkProperties(); + List getScope(); + oneway void requestScope(in List packages, IXposedScopeCallback callback); + void removeScope(in List packages); + List getRunningTargets(); + void hotReloadModule(long targetId, in Bundle data, IHotReloadCallback callback); + Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback); + void updateRemotePreferences(String group, in Bundle diff); + + void deleteRemotePreferences(String group); + ParcelFileDescriptor openRemoteFile(String path); String[] getRemoteFileList(); + + boolean deleteRemoteFile(String path); } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt index b588d863d..a37f90ec2 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -2,6 +2,7 @@ package org.matrix.vector.impl.core import android.os.Build import android.os.Bundle +import android.os.IBinder import android.os.Process import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedInterface @@ -75,6 +76,10 @@ object VectorModuleManager { service = module.service, // Our IPC client ) + // The module service provider only runs in the module app. Deliver an equivalent + // binder directly to module code loaded in this process. + injectXposedService(module, moduleClassLoader, vectorContext) + val entries = instantiateEntries(module, moduleClassLoader, vectorContext) entries.forEach { moduleInstance -> VectorLifecycleManager.activeModules.add(moduleInstance) @@ -171,6 +176,7 @@ object VectorModuleManager { applicationInfo = module.applicationInfo, service = module.service, ) + injectXposedService(module, moduleClassLoader, vectorContext) newEntries = instantiateEntries(module, moduleClassLoader, vectorContext) if (newEntries.size != module.file.moduleClassNames.size) { throw IllegalStateException("Failed to instantiate hot reload entry") @@ -210,6 +216,32 @@ object VectorModuleManager { } } + private fun injectXposedService( + module: Module, + moduleClassLoader: ClassLoader, + vectorContext: VectorContext, + ) { + if (module.file.legacy) return + runCatching { + val helperClass = + moduleClassLoader.loadClass( + "io.github.libxposed.service.XposedServiceHelper" + ) + val onBinderReceivedMethod = + helperClass.getDeclaredMethod("onBinderReceived", IBinder::class.java) + onBinderReceivedMethod.isAccessible = true + val xposedService = VectorXposedService( + module.packageName, + vectorContext, + module.service, + ) + onBinderReceivedMethod.invoke(null, xposedService.asBinder()) + Log.d(TAG, "Injected XposedService for ${module.packageName}") + }.onFailure { e -> + Log.w(TAG, "Failed to inject XposedService for ${module.packageName}", e) + } + } + private fun buildLibrarySearchPath(module: Module): String = buildString { val abis = if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorXposedService.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorXposedService.kt new file mode 100644 index 000000000..3cdb1306b --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorXposedService.kt @@ -0,0 +1,147 @@ +package org.matrix.vector.impl.core + +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import io.github.libxposed.service.HookedProcess +import io.github.libxposed.service.IHotReloadCallback +import io.github.libxposed.service.IXposedScopeCallback +import io.github.libxposed.service.IXposedService +import java.io.Serializable +import org.lsposed.lspd.service.ILSPInjectedModuleService +import org.lsposed.lspd.util.Utils.Log +import org.matrix.vector.impl.VectorContext + +/** + * Exposes the libxposed service API to module code loaded in a hooked process. + * + * The upstream service provider delivers this binder to the module app. Hooked processes do not + * host that provider, so [VectorModuleManager] injects this adapter into the module classloader and + * forwards its operations to the daemon through [ILSPInjectedModuleService]. + */ +internal class VectorXposedService( + private val packageName: String, + private val context: VectorContext, + private val injectedService: ILSPInjectedModuleService?, +) : IXposedService.Stub() { + + override fun getApiVersion(): Int = IXposedService.LIB_API + + override fun getFrameworkName(): String = context.frameworkName + + override fun getFrameworkVersion(): String = context.frameworkVersion + + override fun getFrameworkVersionCode(): Long = context.frameworkVersionCode + + override fun getFrameworkProperties(): Long = context.frameworkProperties + + override fun getScope(): List = injectedService?.scope ?: emptyList() + + override fun requestScope( + packages: MutableList?, + callback: IXposedScopeCallback?, + ) { + if (packages == null || callback == null) return + injectedService?.requestScope(packages, callback) + ?: callback.onScopeRequestFailed("Injected module service unavailable") + } + + override fun removeScope(packages: MutableList?) { + if (packages != null) injectedService?.removeScope(packages) + } + + override fun getRunningTargets(): MutableList = + injectedService?.runningTargets?.toMutableList() ?: mutableListOf() + + override fun hotReloadModule( + targetId: Long, + data: Bundle?, + callback: IHotReloadCallback?, + ) { + injectedService?.hotReloadModule(targetId, data, callback) + ?: callback?.onHotReloadResult( + IXposedService.HOT_RELOAD_UNSUPPORTED, + "Injected module service unavailable", + ) + } + + override fun requestRemotePreferences(group: String?): Bundle { + if (group == null) return Bundle().apply { + putSerializable("map", java.util.HashMap()) + } + return try { + injectedService?.requestRemotePreferences(group, null) + ?: Bundle().apply { putSerializable("map", java.util.HashMap()) } + } catch (e: RemoteException) { + Log.e( + TAG, + "Failed to request remote preferences for $packageName / $group", + e, + ) + Bundle().apply { putSerializable("map", java.util.HashMap()) } + } + } + + override fun updateRemotePreferences(group: String?, diff: Bundle?) { + if (group == null || diff == null) return + try { + (injectedService ?: throw RemoteException("Injected module service unavailable")) + .updateRemotePreferences(group, diff) + } catch (e: RemoteException) { + throw RemoteException( + "Failed to update remote preferences for $packageName / $group: ${e.message}" + ) + } catch (e: UnsupportedOperationException) { + throw RemoteException( + "Remote preference updates are not supported: ${e.message}" + ) + } + } + + override fun deleteRemotePreferences(group: String?) { + if (group == null) return + try { + (injectedService ?: throw RemoteException("Injected module service unavailable")) + .deleteRemotePreferences(group) + } catch (e: RemoteException) { + throw RemoteException( + "Failed to delete remote preferences for $packageName / $group: ${e.message}" + ) + } + } + + override fun listRemoteFiles(): Array { + return try { + injectedService?.remoteFileList ?: emptyArray() + } catch (e: RemoteException) { + Log.e(TAG, "Failed to list remote files for $packageName", e) + emptyArray() + } + } + + override fun openRemoteFile(name: String?): ParcelFileDescriptor { + if (name.isNullOrEmpty()) throw RemoteException("File name must not be empty") + try { + return injectedService?.openRemoteFile(name) + ?: throw RemoteException("Injected module service unavailable") + } catch (e: RemoteException) { + throw e + } catch (e: Exception) { + throw RemoteException("Cannot open remote file: $name — ${e.message}") + } + } + + override fun deleteRemoteFile(name: String?): Boolean { + if (name.isNullOrEmpty()) return false + return try { + injectedService?.deleteRemoteFile(name) ?: false + } catch (e: RemoteException) { + Log.e(TAG, "Failed to delete remote file: $name", e) + false + } + } + + companion object { + private const val TAG = "VectorXposedService" + } +}