Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, MutableSet<IRemotePreferenceCallback>>()

override fun getFrameworkProperties(): Long {
Expand All @@ -30,6 +35,55 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul
return prop
}

override fun getScope(): List<String> =
ConfigCache.getModuleScope(packageName)?.map { it.packageName } ?: emptyList()

override fun requestScope(packages: List<String>, 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<String>) {
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<HookedProcess> {
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?
Expand All @@ -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<String, Any?>()
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) }
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() {
val userId = ensureModule()
val values = mutableMapOf<String, Any?>()

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 }
}
Expand All @@ -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<String> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> getScope();
oneway void requestScope(in List<String> packages, IXposedScopeCallback callback);
void removeScope(in List<String> packages);
List<HookedProcess> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading