diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ad7a2288..b883c23b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,6 @@ plugins { alias(libs.plugins.nav.safeargs) alias(libs.plugins.autoresconfig) alias(libs.plugins.materialthemebuilder) - alias(libs.plugins.lsplugin.resopt) alias(libs.plugins.lsplugin.apksign) } diff --git a/app/src/main/java/org/lsposed/manager/ConfigManager.java b/app/src/main/java/org/lsposed/manager/ConfigManager.java index 9be97ba5e..cbef009f8 100644 --- a/app/src/main/java/org/lsposed/manager/ConfigManager.java +++ b/app/src/main/java/org/lsposed/manager/ConfigManager.java @@ -336,15 +336,6 @@ public static boolean setHiddenIcon(boolean hide) { } } - public static String getApi() { - try { - return LSPManagerServiceHolder.getService().getApi(); - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return e.toString(); - } - } - public static List getDenyListPackages() { List list = new ArrayList<>(); try { diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java index ac9b9d450..438150e81 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java @@ -150,8 +150,8 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp binding.statusTitle.setText(R.string.activated); binding.statusIcon.setImageResource(R.drawable.ic_round_check_circle_24); } - binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", - ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi())); + binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", + ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); binding.developerWarningCard.setVisibility(isDeveloper() ? View.VISIBLE : View.GONE); } else { boolean isMagiskInstalled = ConfigManager.isMagiskInstalled(); diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java index ef8c5f728..c077074b3 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java @@ -80,7 +80,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c getChildFragmentManager().beginTransaction().add(R.id.setting_container, new PreferenceFragment()).commitNow(); } if (ConfigManager.isBinderAlive()) { - binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi())); + binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); } else { binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed))); } diff --git a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java index 8e7285419..525149815 100644 --- a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java +++ b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java @@ -55,13 +55,12 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) return; var body = response.body(); if (body == null) return; - String api = ConfigManager.isBinderAlive() ? ConfigManager.getApi() : "riru"; try { var info = JsonParser.parseReader(body.charStream()).getAsJsonObject(); var notes = info.get("body").getAsString(); var assetsArray = info.getAsJsonArray("assets"); for (var assets : assetsArray) { - checkAssets(assets.getAsJsonObject(), notes, api.toLowerCase(Locale.ROOT)); + checkAssets(assets.getAsJsonObject(), notes); } } catch (Throwable t) { Log.e(App.TAG, t.getMessage(), t); @@ -79,11 +78,10 @@ public void onFailure(@NonNull Call call, @NonNull IOException e) { App.getOkHttpClient().newCall(request).enqueue(callback); } - private static void checkAssets(JsonObject assets, String releaseNotes, String api) { + private static void checkAssets(JsonObject assets, String releaseNotes) { var pref = App.getPreferences(); var name = assets.get("name").getAsString(); var splitName = name.split("-"); - if (!splitName[3].equals(api)) return; pref.edit() .putInt("latest_version", Integer.parseInt(splitName[2])) .putLong("latest_check", Instant.now().getEpochSecond()) diff --git a/build.gradle.kts b/build.gradle.kts index d1d1c12e4..bb697ba2c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -161,6 +161,7 @@ tasks.register("format") { "hiddenapi/*/build.gradle.kts", "services/*-service/build.gradle.kts", ) + dependsOn(":daemon:ktfmtFormat") dependsOn(":xposed:ktfmtFormat") dependsOn(":zygisk:ktfmtFormat") } diff --git a/daemon/README.md b/daemon/README.md new file mode 100644 index 000000000..52a5bf34d --- /dev/null +++ b/daemon/README.md @@ -0,0 +1,63 @@ +# Vector Daemon + +The Vector `daemon` is a highly privileged, standalone executable that runs as `root`. +It acts as the central coordinator and backend for the entire Vector framework. + +Unlike the injected framework code, the daemon does not hook methods directly. Instead, it manages state, provides IPC endpoints to hooked apps and modules, handles AOT compilation evasion, and interacts safely with Android system services. + +## Architecture Overview + +The daemon relies on a dual-IPC architecture and extensive use of Android Binder mechanisms to orchestrate the framework lifecycle without triggering SELinux denials or breaking system stability. + +1. _Bootstrapping & Bridge (`core/`)_: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service. +2. _Privileged IPC Provider (`ipc/`)_: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: + * _Framework Loader DEX_: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. + * _Obfuscation Maps_: Dictionaries provided over IPC when API protection is enabled, allowing the injected code to correctly resolve the randomized class names at runtime. + * _Dynamic Module Scopes_: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName. +3. _State Management (`data/`)_: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an _Immutable State Container_ (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution. +4. _Native Environment (`env/` & JNI)_: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. + +## Directory Layout + +```text +src/main/ +├── kotlin/org/matrix/vector/daemon/ +│ ├── core/ # Entry point (Main), looper setup, and OS broadcast receivers +│ ├── ipc/ # AIDL implementations (Manager, Module, App, SystemServer endpoints) +│ ├── data/ # SQLite DB, Immutable State (DaemonState, ConfigCache), PreferenceStore, File & ZIP parsing +│ ├── system/ # System binder wrappers, UID observers, Notification UI +│ ├── env/ # Socket servers and monitors communicating with JNI (dex2oat, logcat) +│ └── utils/ # OEM-specific workarounds, FakeContext, JNI bindings +└── jni/ # Native C++ layer (dex2oat wrapper, logcat watcher, slicer obfuscation) +``` + +## Core Technical Mechanisms + +### 1. IPC Routing (The Two Doors) +* _Door 1 (`SystemServerService`)_: A native-to-native entry point used exclusively for the _System-Level Initialization_ of `system_server`. By proxying the hardware `serial` service (via `IServiceCallback`), the daemon provides a rendezvous point accessible to the system before the Activity Manager is even initialized. It handles raw UID/PID/Heartbeat packets to authorize the base system framework hook. +* _Door 2 (`VectorService`)_: The _Application-Level Entrance_ used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol allowing the daemon to perform _Scope Filtering_—matching the calling process against the current `DaemonState` before granting access to the framework. + +### 2. AOT Compilation Hijacking (`dex2oat`) +To prevent Android's ART from inlining hooked methods (which makes them unhookable), Vector hijacks the Ahead-of-Time (AOT) compiler. +* _Mechanism_: The daemon (`Dex2OatServer`) mounts a C++ wrapper binary (`bin/dex2oatXX`) over the system's actual `dex2oat` binaries in the `/apex` mount namespace. +* _FD Passing_: When the wrapper executes, to read the original compiler or the `liboat_hook.so`, it opens a UNIX domain socket to the daemon. The daemon (running as root) opens the files and passes the File Descriptors (FDs) back to the wrapper via `SCM_RIGHTS`. +* _Execution_: The wrapper uses `memfd_create` and `sendfile` to load the hook, bypassing execute restrictions, and uses `LD_PRELOAD` to inject the hook into the real `dex2oat` process while appending `--inline-max-code-units=0`. + +### 3. API Protection & DEX Obfuscation +To prevent unauthorized apps from detecting the framework or invoking the Xposed API, the daemon randomizes framework and loader class names on each boot. JNI maps the input `SharedMemory` via `MAP_SHARED` to gain direct, zero-copy access to the physical pages populated by Java. Using the [DexBuilder](https://github.com/JingMatrix/DexBuilder) library, the daemon mutates the DEX string pool in-place; this is highly efficient as the library's Intermediate Representation points directly to the mapped buffer, avoiding unnecessary heap allocations during the randomization process. + +Once mutation is complete, the finalized DEX is written into a new `SharedMemory` region and the original plaintext handle is closed. Because signatures are now randomized, the daemon provides _Obfuscation Maps_ via Door 1 and Door 2. These dictionaries allow the injected code to correctly "re-link" and resolve the framework's internal classes at runtime despite their randomized names. + +### 4. Lifecycle & Process Injection +Vector uses a proactive _Push Model_ to distribute the `IXposedService` binder. Upon detecting a process start via `IUidObserver`, the daemon utilizes `getContentProviderExternal` to obtain a direct line to the module's internal provider. It then executes a synchronous `IContentProvider.call()`, passing the control binder within a `Bundle`. This ensures the framework reference is injected into the target process’s memory before its `Application.onCreate()` executes, bypassing the detection and latency associated with standard `bindService` calls. + +_Remote Preferences & Files_ are supported by a combination of the injected Binder and custom SELinux types. The daemon stores preferences and shared files in directories labeled `xposed_data`. Because the policy allows global access to this type, the injected binder simply provides the path or File Descriptor, and the target app can perform direct I/O, bypassing standard per-app sandbox restrictions. + +## Development & Maintenance Guidelines + +When modifying the daemon, strictly adhere to the following principles: + +1. _Never Block IPC Threads_: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the lock-free, immutable `DaemonState` snapshot exposed by `ConfigCache.state`. +2. _Resource Determinism_: The daemon runs indefinitely. Leaking a single `Cursor`, `ParcelFileDescriptor`, or `SharedMemory` instance will eventually exhaust system limits and crash the OS. Always use Kotlin's `.use { }` blocks or explicit C++ RAII wrappers for native resources. +3. _Isolate OEM Quirks_: Android OS behavior varies wildly between manufacturers (e.g., Lenovo hiding cloned apps in user IDs 900-909, MIUI killing background dual-apps). Place all OEM-specific logic in `utils/Workarounds.kt` to prevent core logic pollution. +4. _Context Forgery (`FakeContext`)_: The daemon does not have a real Android `Context`. To interact with system APIs that require one (like building Notifications or querying packages), use `FakeContext`. Be aware that standard `Context` methods may crash if not explicitly mocked. diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index cee0e4d42..40b7e7b61 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -1,6 +1,7 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.ide.common.signing.KeystoreHelper import java.io.PrintStream +import java.util.UUID val defaultManagerPackageName: String by rootProject.extra val injectedPackageName: String by rootProject.extra @@ -9,98 +10,102 @@ val versionCodeProvider: Provider by rootProject.extra val versionNameProvider: Provider by rootProject.extra plugins { - alias(libs.plugins.agp.app) - alias(libs.plugins.lsplugin.resopt) + alias(libs.plugins.agp.app) + alias(libs.plugins.kotlin) + alias(libs.plugins.ktfmt) } android { - buildFeatures { - prefab = true - buildConfig = true - } + defaultConfig { + buildConfigField( + "String", + "DEFAULT_MANAGER_PACKAGE_NAME", + """"$defaultManagerPackageName"""", + ) + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") + buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") + buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) - defaultConfig { - applicationId = "org.lsposed.daemon" + val cliToken = UUID.randomUUID() + // Inject the MSB and LSB as Long constants + buildConfigField("Long", "CLI_TOKEN_MSB", "${cliToken.mostSignificantBits}L") + buildConfigField("Long", "CLI_TOKEN_LSB", "${cliToken.leastSignificantBits}L") + } - buildConfigField( - "String", - "DEFAULT_MANAGER_PACKAGE_NAME", - """"$defaultManagerPackageName"""", - ) - buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") - buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") - buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") - buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") - buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) + buildTypes { + all { externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } } + release { + isMinifyEnabled = true + proguardFiles("proguard-rules.pro") } + } - buildTypes { - all { - externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } - } - release { - isMinifyEnabled = true - isShrinkResources = true - proguardFiles("proguard-rules.pro") - } - } + externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } - externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } - - namespace = "org.lsposed.daemon" + namespace = "org.matrix.vector.daemon" } android.applicationVariants.all { - val variantCapped = name.replaceFirstChar { it.uppercase() } - val variantLowered = name.lowercase() + val variantCapped = name.replaceFirstChar { it.uppercase() } + val variantLowered = name.lowercase() + + val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get() + val signInfoTask = + tasks.register("generate${variantCapped}SignInfo") { + dependsOn(":app:validateSigning${variantCapped}") + val sign = + rootProject + .project(":app") + .extensions + .getByType(ApplicationExtension::class.java) + .buildTypes + .named(variantLowered) + .get() + .signingConfig + val outSrc = file("$outSrcDir/org/matrix/vector/daemon/utils/SignInfo.kt") + outputs.file(outSrc) + doLast { + outSrc.parentFile.mkdirs() + val certificateInfo = + KeystoreHelper.getCertificateInfo( + sign?.storeType, + sign?.storeFile, + sign?.storePassword, + sign?.keyPassword, + sign?.keyAlias, + ) - val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get() - val signInfoTask = - tasks.register("generate${variantCapped}SignInfo") { - dependsOn(":app:validateSigning${variantCapped}") - val sign = - rootProject - .project(":app") - .extensions - .getByType(ApplicationExtension::class.java) - .buildTypes - .named(variantLowered) - .get() - .signingConfig - val outSrc = file("$outSrcDir/org/lsposed/lspd/util/SignInfo.java") - outputs.file(outSrc) - doLast { - outSrc.parentFile.mkdirs() - val certificateInfo = - KeystoreHelper.getCertificateInfo( - sign?.storeType, - sign?.storeFile, - sign?.storePassword, - sign?.keyPassword, - sign?.keyAlias, - ) - PrintStream(outSrc) - .print( - """ - |package org.lsposed.lspd.util; - |public final class SignInfo { - | public static final byte[] CERTIFICATE = {${ + PrintStream(outSrc) + .print( + """ + |package org.matrix.vector.daemon.utils + | + |object SignInfo { + | @JvmField + | val CERTIFICATE = byteArrayOf(${ certificateInfo.certificate.encoded.joinToString(",") - }}; + }) |}""" - .trimMargin() - ) - } + .trimMargin()) } - registerJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + } + // registeoJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + + kotlin.sourceSets.getByName(variantLowered) { kotlin.srcDir(signInfoTask.map { outSrcDir }) } } dependencies { - implementation(libs.agp.apksig) - implementation(projects.external.apache) - implementation(projects.hiddenapi.bridge) - implementation(projects.services.daemonService) - implementation(projects.services.managerService) - compileOnly(libs.androidx.annotation) - compileOnly(projects.hiddenapi.stubs) + implementation(libs.agp.apksig) + implementation(libs.gson) + implementation(libs.picocli) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + implementation(projects.external.apache) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) + implementation(projects.services.managerService) + compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) } diff --git a/daemon/proguard-rules.pro b/daemon/proguard-rules.pro index 74c73f81e..4f1bd0b1a 100644 --- a/daemon/proguard-rules.pro +++ b/daemon/proguard-rules.pro @@ -1,17 +1,13 @@ -keepclasseswithmembers,includedescriptorclasses class * { native ; } --keepclasseswithmembers class org.lsposed.lspd.Main { +-keepclasseswithmembers class org.matrix.vector.daemon.VectorDaemon { public static void main(java.lang.String[]); } --keepclasseswithmembers class org.lsposed.lspd.service.Dex2OatService { - private java.lang.String devTmpDir; - private java.lang.String magiskPath; - private java.lang.String fakeBin32; - private java.lang.String fakeBin64; - private java.lang.String[] dex2oatBinaries; +-keepclasseswithmembers class org.matrix.vector.daemon.Cli { + public static void main(java.lang.String[]); } --keepclasseswithmembers class org.lsposed.lspd.service.LogcatService { +-keepclasseswithmembers class org.matrix.vector.daemon.env.LogcatMonitor { private int refreshFd(boolean); } -keepclassmembers class ** implements android.content.ContextWrapper { diff --git a/daemon/src/main/java/org/lsposed/lspd/Main.java b/daemon/src/main/java/org/lsposed/lspd/Main.java deleted file mode 100644 index 3263091f1..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/Main.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.lsposed.lspd; - -import org.lsposed.lspd.service.ServiceManager; - -public class Main { - - public static void main(String[] args) { - ServiceManager.start(args); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java deleted file mode 100644 index 04fbe6a4a..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.ContentProviderHolder; -import android.app.IActivityManager; -import android.app.IApplicationThread; -import android.app.IServiceConnection; -import android.app.IUidObserver; -import android.app.ProfilerInfo; -import android.content.Context; -import android.content.IContentProvider; -import android.content.IIntentReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.UserInfo; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class ActivityManagerService { - private static IActivityManager am = null; - private static IBinder binder = null; - private static IApplicationThread appThread = null; - private static IBinder token = null; - - private static final IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "am is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - am = null; - appThread = null; - token = null; - } - }; - - public static IActivityManager getActivityManager() { - if (binder == null || am == null) { - binder = ServiceManager.getService(Context.ACTIVITY_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(deathRecipient, 0); - am = IActivityManager.Stub.asInterface(binder); - // For oddo Android 9 we cannot set activity controller here... - // am.setActivityController(null, false); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return am; - } - - public static int broadcastIntentWithFeature(String callingFeatureId, - Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, - String resultData, Bundle map, String[] requiredPermissions, - int appOp, Bundle options, boolean serialized, boolean sticky, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, - resultCode, resultData, null, requiredPermissions, null, null, appOp, null, - serialized, sticky, userId); - } catch (NoSuchMethodError ignored) { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, - resultCode, resultData, null, requiredPermissions, null, appOp, null, - serialized, sticky, userId); - } - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermissions, appOp, options, serialized, sticky, userId); - } else { - return am.broadcastIntent(appThread, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermissions, appOp, options, serialized, sticky, userId); - } - } - - public static void forceStopPackage(String packageName, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return; - am.forceStopPackage(packageName, userId); - } - - public static boolean startUserInBackground(int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.startUserInBackground(userId); - } - - public static Intent registerReceiver(String callerPackage, - String callingFeatureId, IIntentReceiver receiver, IntentFilter filter, - String requiredPermission, int userId, int flags) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return null; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - return am.registerReceiverWithFeature(appThread, callerPackage, callingFeatureId, "null", receiver, filter, requiredPermission, userId, flags); - else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - return am.registerReceiverWithFeature(appThread, callerPackage, callingFeatureId, receiver, filter, requiredPermission, userId, flags); - } else { - return am.registerReceiver(appThread, callerPackage, receiver, filter, requiredPermission, userId, flags); - } - } - - public static void finishReceiver(IBinder intentReceiver, IBinder applicationThread, int resultCode, - String resultData, Bundle resultExtras, boolean resultAbort, - int flags) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - am.finishReceiver(applicationThread, resultCode, resultData, resultExtras, resultAbort, flags); - } else { - am.finishReceiver(intentReceiver, resultCode, resultData, resultExtras, resultAbort, flags); - } - } - - public static int bindService(Intent service, - String resolvedType, IServiceConnection connection, int flags, - String callingPackage, int userId) throws RemoteException { - - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - return am.bindService(appThread, token, service, resolvedType, connection, (long) flags, callingPackage, userId); - else - return am.bindService(appThread, token, service, resolvedType, connection, flags, callingPackage, userId); - } - - public static boolean unbindService(IServiceConnection connection) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.unbindService(connection); - } - - public static int startActivityAsUserWithFeature(String callingPackage, - String callingFeatureId, Intent intent, String resolvedType, - IBinder resultTo, String resultWho, int requestCode, int flags, - ProfilerInfo profilerInfo, Bundle options, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return am.startActivityAsUserWithFeature(appThread, callingPackage, callingFeatureId, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options, userId); - } else { - return am.startActivityAsUser(appThread, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options, userId); - } - } - - public static void onSystemServerContext(IApplicationThread thread, IBinder token) { - ActivityManagerService.appThread = thread; - ActivityManagerService.token = token; - } - - public static boolean switchUser(int userid) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.switchUser(userid); - } - - public static UserInfo getCurrentUser() throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - return am.getCurrentUser(); - } - - public static Configuration getConfiguration() throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - return am.getConfiguration(); - } - - public static IContentProvider getContentProvider(String auth, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - ContentProviderHolder holder; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - holder = am.getContentProviderExternal(auth, userId, token, null); - } else { - holder = am.getContentProviderExternal(auth, userId, token); - } - return holder != null ? holder.provider : null; - } - - public static void registerUidObserver(IUidObserver observer, int which, int cutpoint, String callingPackage) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return; - am.registerUidObserver(observer, which, cutpoint, callingPackage); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java deleted file mode 100644 index 850cc4d08..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.ActivityManager; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Parcel; -import android.os.ServiceManager; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; - -import org.lsposed.daemon.BuildConfig; - -import java.lang.reflect.Field; -import java.util.Map; - -public class BridgeService { - - static final int TRANSACTION_CODE = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; - private static final String SERVICE_NAME = "activity"; - - enum ACTION { - ACTION_UNKNOWN, - ACTION_SEND_BINDER, - ACTION_GET_BINDER, - } - - public interface Listener { - void onSystemServerRestarted(); - - void onResponseFromBridgeService(boolean response); - - void onSystemServerDied(); - } - - private static IBinder serviceBinder = null; - - private static Listener listener; - private static IBinder bridgeService; - private static final IBinder.DeathRecipient bridgeRecipient = new IBinder.DeathRecipient() { - - @Override - public void binderDied() { - Log.i(TAG, "service " + SERVICE_NAME + " is dead. "); - - try { - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - Field field = ServiceManager.class.getDeclaredField("sServiceManager"); - field.setAccessible(true); - field.set(null, null); - - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - field = ServiceManager.class.getDeclaredField("sCache"); - field.setAccessible(true); - Object sCache = field.get(null); - if (sCache instanceof Map) { - //noinspection rawtypes - ((Map) sCache).clear(); - } - Log.i(TAG, "clear ServiceManager"); - - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - field = ActivityManager.class.getDeclaredField("IActivityManagerSingleton"); - field.setAccessible(true); - Object singleton = field.get(null); - if (singleton != null) { - //noinspection PrivateApi DiscouragedPrivateApi - field = Class.forName("android.util.Singleton").getDeclaredField("mInstance"); - field.setAccessible(true); - synchronized (singleton) { - field.set(singleton, null); - } - } - Log.i(TAG, "clear ActivityManager"); - } catch (Throwable e) { - Log.w(TAG, "clear ServiceManager: " + Log.getStackTraceString(e)); - } - - bridgeService.unlinkToDeath(this, 0); - bridgeService = null; - listener.onSystemServerDied(); - new Handler(Looper.getMainLooper()).post(() -> sendToBridge(serviceBinder, true)); - } - }; - - // For service - // This MUST run in main thread - private static synchronized void sendToBridge(IBinder binder, boolean isRestart) { - assert Looper.myLooper() == Looper.getMainLooper(); - try { - Os.seteuid(0); - } catch (ErrnoException e) { - Log.e(TAG, "seteuid 0", e); - } - try { - do { - bridgeService = ServiceManager.getService(SERVICE_NAME); - if (bridgeService != null && bridgeService.pingBinder()) { - break; - } - - Log.i(TAG, "service " + SERVICE_NAME + " is not started, wait 1s."); - - try { - //noinspection BusyWait - Thread.sleep(1000); - } catch (Throwable e) { - Log.w(TAG, "sleep" + Log.getStackTraceString(e)); - } - } while (true); - - if (isRestart && listener != null) { - listener.onSystemServerRestarted(); - } - - try { - bridgeService.linkToDeath(bridgeRecipient, 0); - } catch (Throwable e) { - Log.w(TAG, "linkToDeath " + Log.getStackTraceString(e)); - var snapshot = bridgeService; - sendToBridge(binder, snapshot == null || !snapshot.isBinderAlive()); - return; - } - - boolean res = false; - // try at most three times - for (int i = 0; i < 3; i++) { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - try { - data.writeInt(ACTION.ACTION_SEND_BINDER.ordinal()); - Log.v(TAG, "binder " + binder.toString()); - data.writeStrongBinder(binder); - if (bridgeService == null) break; - res = bridgeService.transact(TRANSACTION_CODE, data, reply, 0); - reply.readException(); - } catch (Throwable e) { - Log.e(TAG, "send binder " + Log.getStackTraceString(e)); - var snapshot = bridgeService; - sendToBridge(binder, snapshot == null || !snapshot.isBinderAlive()); - return; - } finally { - data.recycle(); - reply.recycle(); - } - - if (res) break; - - Log.w(TAG, "no response from bridge, retry in 1s"); - - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } - } - - if (listener != null) { - listener.onResponseFromBridgeService(res); - } - } finally { - try { - if (!BuildConfig.DEBUG) { - Os.seteuid(1000); - } - } catch (ErrnoException e) { - Log.e(TAG, "seteuid 1000", e); - } - } - } - - public static void send(LSPosedService service, Listener listener) { - BridgeService.listener = listener; - BridgeService.serviceBinder = service.asBinder(); - sendToBridge(serviceBinder, false); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java deleted file mode 100644 index 1f4d08e09..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java +++ /dev/null @@ -1,518 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.toGlobalNamespace; - -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.os.Binder; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SharedMemory; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.PreLoadedApk; -import org.lsposed.lspd.util.InstallerVerifier; -import org.lsposed.lspd.util.Utils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Method; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.stream.Stream; -import java.util.zip.Deflater; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import hidden.HiddenApiBridge; - -public class ConfigFileManager { - static final Path basePath = Paths.get("/data/adb/lspd"); - static final Path modulePath = basePath.resolve("modules"); - static final Path daemonApkPath = Paths.get(System.getProperty("java.class.path", null)); - static final Path managerApkPath = daemonApkPath.getParent().resolve("manager.apk"); - static final File magiskDbPath = new File("/data/adb/magisk.db"); - private static final Path lockPath = basePath.resolve("lock"); - private static final Path configDirPath = basePath.resolve("config"); - static final File dbPath = configDirPath.resolve("modules_config.db").toFile(); - private static final Path logDirPath = basePath.resolve("log"); - private static final Path oldLogDirPath = basePath.resolve("log.old"); - private static final DateTimeFormatter formatter = - DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(Utils.getZoneId()); - @SuppressWarnings("FieldCanBeLocal") - private static FileLocker locker = null; - private static Resources res = null; - private static ParcelFileDescriptor fd = null; - private static SharedMemory preloadDex = null; - - static { - try { - Files.createDirectories(basePath); - SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0"); - Files.createDirectories(configDirPath); - createLogDirPath(); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - public static void transfer(InputStream in, OutputStream out) throws IOException { - int size = 8192; - var buffer = new byte[size]; - int read; - while ((read = in.read(buffer, 0, size)) >= 0) { - out.write(buffer, 0, read); - } - } - - private static void createLogDirPath() throws IOException { - if (!Files.isDirectory(logDirPath, LinkOption.NOFOLLOW_LINKS)) { - Files.deleteIfExists(logDirPath); - } - Files.createDirectories(logDirPath); - } - - public static Resources getResources() { - loadRes(); - return res; - } - - private static void loadRes() { - if (res != null) return; - try { - var am = AssetManager.class.newInstance(); - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); - addAssetPath.setAccessible(true); - //noinspection ConstantConditions - if ((int) addAssetPath.invoke(am, daemonApkPath.toString()) > 0) { - //noinspection deprecation - res = new Resources(am, null, null); - } - } catch (Throwable e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - static void reloadConfiguration() { - loadRes(); - try { - var conf = ActivityManagerService.getConfiguration(); - if (conf != null) - //noinspection deprecation - res.updateConfiguration(conf, res.getDisplayMetrics()); - } catch (Throwable e) { - Log.e(TAG, "reload configuration", e); - } - } - - static ParcelFileDescriptor getManagerApk() throws IOException { - if (fd != null) return fd.dup(); - InstallerVerifier.verifyInstallerSignature(managerApkPath.toString()); - - SELinux.setFileContext(managerApkPath.toString(), "u:object_r:system_file:s0"); - fd = ParcelFileDescriptor.open(managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY); - return fd.dup(); - } - - static void deleteFolderIfExists(Path target) throws IOException { - if (Files.notExists(target)) return; - Files.walkFileTree(target, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } else { - throw e; - } - } - }); - } - - public static boolean chattr0(Path path) { - try { - var dir = Os.open(path.toString(), OsConstants.O_RDONLY, 0); - // Clear all special file attributes on the directory - HiddenApiBridge.Os_ioctlInt(dir, Process.is64Bit() ? 0x40086602 : 0x40046602, 0); - Os.close(dir); - return true; - } catch (ErrnoException e) { - // If the operation is not supported (ENOTSUP), it means the filesystem doesn't support attributes. - // We can assume the file is not immutable and proceed. - if (e.errno == OsConstants.ENOTSUP) { - return true; - } - Log.d(TAG, "chattr 0", e); - return false; - } catch (Throwable e) { - Log.d(TAG, "chattr 0", e); - return false; - } - } - - static void moveLogDir() { - try { - if (Files.exists(logDirPath)) { - if (chattr0(logDirPath)) { - deleteFolderIfExists(oldLogDirPath); - Files.move(logDirPath, oldLogDirPath); - } - } - Files.createDirectories(logDirPath); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - private static String getNewLogFileName(String prefix) { - return prefix + "_" + formatter.format(Instant.now()) + ".log"; - } - - static File getNewVerboseLogPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve(getNewLogFileName("verbose")).toFile(); - } - - static File getNewModulesLogPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve(getNewLogFileName("modules")).toFile(); - } - - static File getPropsPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve("props.txt").toFile(); - } - - static File getKmsgPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve("kmsg.log").toFile(); - } - - static void getLogs(ParcelFileDescriptor zipFd) throws IllegalStateException { - try (zipFd; var os = new ZipOutputStream(new FileOutputStream(zipFd.getFileDescriptor()))) { - var comment = String.format(Locale.ROOT, "LSPosed %s %s (%d)", - BuildConfig.BUILD_TYPE, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); - os.setComment(comment); - os.setLevel(Deflater.BEST_COMPRESSION); - zipAddDir(os, logDirPath); - zipAddDir(os, oldLogDirPath); - zipAddDir(os, Paths.get("/data/tombstones")); - zipAddDir(os, Paths.get("/data/anr")); - var data = Paths.get("/data/data"); - var app1 = data.resolve(BuildConfig.MANAGER_INJECTED_PKG_NAME + "/cache/crash"); - var app2 = data.resolve(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME + "/cache/crash"); - zipAddDir(os, app1); - zipAddDir(os, app2); - zipAddProcOutput(os, "full.log", "logcat", "-b", "all", "-d"); - zipAddProcOutput(os, "dmesg.log", "dmesg"); - var magiskDataDir = Paths.get("/data/adb"); - try (var l = Files.list(magiskDataDir.resolve("modules"))) { - l.forEach(p -> { - zipAddFile(os, p, magiskDataDir); - zipAddFile(os, p.resolve("module.prop"), magiskDataDir); - zipAddFile(os, p.resolve("remove"), magiskDataDir); - zipAddFile(os, p.resolve("disable"), magiskDataDir); - zipAddFile(os, p.resolve("update"), magiskDataDir); - zipAddFile(os, p.resolve("sepolicy.rule"), magiskDataDir); - }); - } - var proc = Paths.get("/proc"); - for (var pid : new String[]{"self", String.valueOf(Binder.getCallingPid())}) { - var pidPath = proc.resolve(pid); - zipAddFile(os, pidPath.resolve("maps"), proc); - zipAddFile(os, pidPath.resolve("mountinfo"), proc); - zipAddFile(os, pidPath.resolve("status"), proc); - } - zipAddFile(os, dbPath.toPath(), configDirPath); - ConfigManager.getInstance().exportScopes(os); - } catch (Throwable e) { - Log.w(TAG, "get log", e); - throw new IllegalStateException(e); - } - } - - private static void zipAddProcOutput(ZipOutputStream os, String name, String... command) { - try (var is = new ProcessBuilder(command).start().getInputStream()) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - - private static void zipAddFile(ZipOutputStream os, Path path, Path base) { - var name = base.relativize(path).toString(); - if (Files.isDirectory(path)) { - try { - os.putNextEntry(new ZipEntry(name + "/")); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } else if (Files.exists(path)) { - try (var is = new FileInputStream(path.toFile())) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - } - - private static void zipAddDir(ZipOutputStream os, Path path) throws IOException { - if (!Files.isDirectory(path)) return; - Files.walkFileTree(path, new SimpleFileVisitor<>() { - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (Files.isRegularFile(file)) { - var name = path.getParent().relativize(file).toString(); - try (var is = new FileInputStream(file.toFile())) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - return FileVisitResult.CONTINUE; - } - }); - } - - private static SharedMemory readDex(InputStream in, boolean obfuscate) throws IOException, ErrnoException { - var memory = SharedMemory.create(null, in.available()); - var byteBuffer = memory.mapReadWrite(); - Channels.newChannel(in).read(byteBuffer); - SharedMemory.unmap(byteBuffer); - if (obfuscate) { - var newMemory = ObfuscationManager.obfuscateDex(memory); - if (memory != newMemory) { - memory.close(); - memory = newMemory; - } - } - memory.setProtect(OsConstants.PROT_READ); - return memory; - } - - private static void readDexes(ZipFile apkFile, List preLoadedDexes, - boolean obfuscate) { - int secondary = 2; - for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; - dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) { - try (var is = apkFile.getInputStream(dexFile)) { - preLoadedDexes.add(readDex(is, obfuscate)); - } catch (IOException | ErrnoException e) { - Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e); - } - } - } - - private static void readName(ZipFile apkFile, String initName, List names) { - var initEntry = apkFile.getEntry(initName); - if (initEntry == null) return; - try (var in = apkFile.getInputStream(initEntry)) { - var reader = new BufferedReader(new InputStreamReader(in)); - String name; - while ((name = reader.readLine()) != null) { - name = name.trim(); - if (name.isEmpty() || name.startsWith("#")) continue; - names.add(name); - } - } catch (IOException | OutOfMemoryError e) { - Log.e(TAG, "Can not open " + initEntry, e); - } - } - - @Nullable - static PreLoadedApk loadModule(String path, boolean obfuscate) { - if (path == null) return null; - var file = new PreLoadedApk(); - var preLoadedDexes = new ArrayList(); - var moduleClassNames = new ArrayList(1); - var moduleLibraryNames = new ArrayList(1); - try (var apkFile = new ZipFile(toGlobalNamespace(path))) { - readDexes(apkFile, preLoadedDexes, obfuscate); - readName(apkFile, "META-INF/xposed/java_init.list", moduleClassNames); - if (moduleClassNames.isEmpty()) { - file.legacy = true; - readName(apkFile, "assets/xposed_init", moduleClassNames); - readName(apkFile, "assets/native_init", moduleLibraryNames); - } else { - file.legacy = false; - readName(apkFile, "META-INF/xposed/native_init.list", moduleLibraryNames); - } - } catch (IOException e) { - Log.e(TAG, "Can not open " + path, e); - return null; - } - if (preLoadedDexes.isEmpty()) return null; - if (moduleClassNames.isEmpty()) return null; - - if (obfuscate) { - var signatures = ObfuscationManager.getSignatures(); - for (int i = 0; i < moduleClassNames.size(); i++) { - var s = moduleClassNames.get(i); - for (var entry : signatures.entrySet()) { - if (s.startsWith(entry.getKey())) { - moduleClassNames.add(i, s.replace(entry.getKey(), entry.getValue())); - } - } - } - } - - file.preLoadedDexes = preLoadedDexes; - file.moduleClassNames = moduleClassNames; - file.moduleLibraryNames = moduleLibraryNames; - return file; - } - - static boolean tryLock() { - var openOptions = new HashSet(); - openOptions.add(StandardOpenOption.CREATE); - openOptions.add(StandardOpenOption.WRITE); - var p = PosixFilePermissions.fromString("rw-------"); - var permissions = PosixFilePermissions.asFileAttribute(p); - - try { - var lockChannel = FileChannel.open(lockPath, openOptions, permissions); - locker = new FileLocker(lockChannel); - return locker.isValid(); - } catch (Throwable e) { - return false; - } - } - - synchronized static SharedMemory getPreloadDex(boolean obfuscate) { - if (preloadDex == null) { - try (var is = new FileInputStream("framework/lspd.dex")) { - preloadDex = readDex(is, obfuscate); - } catch (Throwable e) { - Log.e(TAG, "preload dex", e); - } - } - return preloadDex; - } - - static void ensureModuleFilePath(String path) throws RemoteException { - if (path == null || path.indexOf(File.separatorChar) >= 0 || ".".equals(path) || "..".equals(path)) { - throw new RemoteException("Invalid path: " + path); - } - } - - static Path resolveModuleDir(String packageName, String dir, int userId, int uid) throws IOException { - var path = modulePath.resolve(String.valueOf(userId)).resolve(packageName).resolve(dir).normalize(); - // Ensure the directory and any necessary parent directories exist. - path.toFile().mkdirs(); - - if (SELinux.getFileContext(path.toString()) != "u:object_r:xposed_data:s0") { - // SELinux label could be reset after a reboot. - try { - setSelinuxContextRecursive(path, "u:object_r:xposed_data:s0"); - Os.chown(path.toString(), uid, uid); - Os.chmod(path.toString(), 0755); - } catch (ErrnoException e) { - throw new IOException(e); - } - } - return path; - } - - private static void setSelinuxContextRecursive(Path path, String context) throws IOException { - try { - SELinux.setFileContext(path.toString(), context); - - if (Files.isDirectory(path)) { - try (Stream stream = Files.list(path)) { - for (Path entry : (Iterable) stream::iterator) { - setSelinuxContextRecursive(entry, context); - } - } - } - } catch (Exception e) { - throw new IOException("Failed to recursively set SELinux context for " + path, e); - } - } - - private static class FileLocker { - private final FileChannel lockChannel; - private final FileLock locker; - - FileLocker(FileChannel lockChannel) throws IOException { - this.lockChannel = lockChannel; - this.locker = lockChannel.tryLock(); - } - - boolean isValid() { - return this.locker != null && this.locker.isValid(); - } - - @Override - protected void finalize() throws Throwable { - this.locker.release(); - this.lockChannel.close(); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java deleted file mode 100644 index 4cdc43903..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ /dev/null @@ -1,1266 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.PackageService.MATCH_ALL_FLAGS; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace; -import static org.lsposed.lspd.service.ServiceManager.toGlobalNamespace; - -import android.annotation.SuppressLint; -import android.content.ContentValues; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageParser; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteStatement; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SharedMemory; -import android.os.SystemClock; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.apache.commons.lang3.SerializationUtilsX; -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.Application; -import org.lsposed.lspd.models.Module; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import hidden.HiddenApiBridge; - -public class ConfigManager { - private static ConfigManager instance = null; - - private final SQLiteDatabase db = openDb(); - - private boolean verboseLog = true; - private boolean logWatchdog = true; - private boolean dexObfuscate = true; - private boolean enableStatusNotification = true; - private Path miscPath = null; - - private int managerUid = -1; - - private final Handler cacheHandler; - - private long lastModuleCacheTime = 0; - private long requestModuleCacheTime = 0; - - private long lastScopeCacheTime = 0; - private long requestScopeCacheTime = 0; - - private String api = "(???)"; - - static class ProcessScope { - final String processName; - final int uid; - - ProcessScope(@NonNull String processName, int uid) { - this.processName = processName; - this.uid = uid; - } - - @Override - public boolean equals(@Nullable Object o) { - if (o instanceof ProcessScope) { - ProcessScope p = (ProcessScope) o; - return p.processName.equals(processName) && p.uid == uid; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hashCode(processName) ^ uid; - } - } - - private static final String CREATE_MODULES_TABLE = "CREATE TABLE IF NOT EXISTS modules (" + - "mid integer PRIMARY KEY AUTOINCREMENT," + - "module_pkg_name text NOT NULL UNIQUE," + - "apk_path text NOT NULL, " + - "enabled BOOLEAN DEFAULT 0 " + - "CHECK (enabled IN (0, 1))" + - ");"; - - private static final String CREATE_SCOPE_TABLE = "CREATE TABLE IF NOT EXISTS scope (" + - "mid integer," + - "app_pkg_name text NOT NULL," + - "user_id integer NOT NULL," + - "PRIMARY KEY (mid, app_pkg_name, user_id)," + - "CONSTRAINT scope_module_constraint" + - " FOREIGN KEY (mid)" + - " REFERENCES modules (mid)" + - " ON DELETE CASCADE" + - ");"; - - private static final String CREATE_CONFIG_TABLE = "CREATE TABLE IF NOT EXISTS configs (" + - "module_pkg_name text NOT NULL," + - "user_id integer NOT NULL," + - "`group` text NOT NULL," + - "`key` text NOT NULL," + - "data blob NOT NULL," + - "PRIMARY KEY (module_pkg_name, user_id, `group`, `key`)," + - "CONSTRAINT config_module_constraint" + - " FOREIGN KEY (module_pkg_name)" + - " REFERENCES modules (module_pkg_name)" + - " ON DELETE CASCADE" + - ");"; - - private final Map> cachedScope = new ConcurrentHashMap<>(); - - // packageName, Module - private final Map cachedModule = new ConcurrentHashMap<>(); - - // packageName, userId, group, key, value - private final Map, Map>> cachedConfig = new ConcurrentHashMap<>(); - - private Set scopeRequestBlocked = new HashSet<>(); - - private static SQLiteDatabase openDb() { - var params = new SQLiteDatabase.OpenParams.Builder() - .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) - .setErrorHandler(sqLiteDatabase -> Log.w(TAG, "database corrupted")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - params.setSynchronousMode("NORMAL"); - } - return SQLiteDatabase.openDatabase(ConfigFileManager.dbPath.getAbsoluteFile(), params.build()); - } - - private void updateCaches(boolean sync) { - synchronized (cacheHandler) { - requestScopeCacheTime = requestModuleCacheTime = SystemClock.elapsedRealtime(); - } - if (sync) { - cacheModules(); - } else { - cacheHandler.post(this::cacheModules); - } - } - - // for system server, cache is not yet ready, we need to query database for it - public boolean shouldSkipSystemServer() { - if (!SELinux.checkSELinuxAccess("u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { - Log.e(TAG, "skip injecting into android because sepolicy was not loaded properly"); - return true; // skip - } - /* - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"modules.mid"}, "app_pkg_name=? AND enabled=1", new String[]{"system"}, null, null, null)) { - return cursor == null || !cursor.moveToNext(); - }*/ - return false; - } - - @SuppressLint("BlockedPrivateApi") - public List getModulesForSystemServer() { - List modules = new LinkedList<>(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"module_pkg_name", "apk_path"}, "app_pkg_name=? AND enabled=1", new String[]{"system"}, null, null, null)) { - int apkPathIdx = cursor.getColumnIndex("apk_path"); - int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - while (cursor.moveToNext()) { - var module = new Module(); - module.apkPath = cursor.getString(apkPathIdx); - module.packageName = cursor.getString(pkgNameIdx); - var cached = cachedModule.get(module.packageName); - if (cached != null) { - modules.add(cached); - continue; - } - var statPath = toGlobalNamespace("/data/user_de/0/" + module.packageName).getAbsolutePath(); - try { - module.appId = Os.stat(statPath).st_uid; - } catch (ErrnoException e) { - Log.w(TAG, "cannot stat " + statPath, e); - module.appId = -1; - } - try { - var apkFile = new File(module.apkPath); - var pkg = new PackageParser().parsePackage(apkFile, 0, false); - module.applicationInfo = pkg.applicationInfo; - module.applicationInfo.sourceDir = module.apkPath; - module.applicationInfo.dataDir = statPath; - module.applicationInfo.deviceProtectedDataDir = statPath; - HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(module.applicationInfo, statPath); - module.applicationInfo.processName = module.packageName; - } catch (PackageParser.PackageParserException e) { - Log.w(TAG, "failed to parse " + module.apkPath, e); - } - module.service = new LSPInjectedModuleService(module.packageName); - modules.add(module); - } - } - - return modules.parallelStream().filter(m -> { - var file = ConfigFileManager.loadModule(m.apkPath, dexObfuscate); - if (file == null) { - Log.w(TAG, "Can not load " + m.apkPath + ", skip!"); - return false; - } - m.file = file; - cachedModule.putIfAbsent(m.packageName, m); - return true; - }).collect(Collectors.toList()); - } - - private synchronized void updateConfig() { - Map config = getModulePrefs("lspd", 0, "config"); - - Object bool = config.get("enable_verbose_log"); - verboseLog = bool == null || (boolean) bool; - - bool = config.get("enable_log_watchdog"); - logWatchdog = bool == null || (boolean) bool; - - bool = config.get("enable_dex_obfuscate"); - dexObfuscate = bool == null || (boolean) bool; - - bool = config.get("enable_auto_add_shortcut"); - if (bool != null) { - // TODO: remove - updateModulePrefs("lspd", 0, "config", "enable_auto_add_shortcut", null); - } - - bool = config.get("enable_status_notification"); - enableStatusNotification = bool == null || (boolean) bool; - - var set = (Set) config.get("scope_request_blocked"); - scopeRequestBlocked = set == null ? new HashSet<>() : set; - - // Don't migrate to ConfigFileManager, as XSharedPreferences will be restored soon - String string = (String) config.get("misc_path"); - if (string == null) { - miscPath = Paths.get("/data", "misc", UUID.randomUUID().toString()); - updateModulePrefs("lspd", 0, "config", "misc_path", miscPath.toString()); - } else { - miscPath = Paths.get(string); - } - try { - var perms = PosixFilePermissions.fromString("rwx--x--x"); - Files.createDirectories(miscPath, PosixFilePermissions.asFileAttribute(perms)); - walkFileTree(miscPath, f -> SELinux.setFileContext(f.toString(), "u:object_r:xposed_data:s0")); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - - updateManager(false); - - cacheHandler.post(this::getPreloadDex); - } - - public synchronized void updateManager(boolean uninstalled) { - if (uninstalled) { - managerUid = -1; - return; - } - if (!PackageService.isAlive()) return; - try { - PackageInfo info = PackageService.getPackageInfo(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0); - if (info != null) { - managerUid = info.applicationInfo.uid; - } else { - managerUid = -1; - Log.i(TAG, "manager is not installed"); - } - } catch (RemoteException ignored) { - } - } - - static ConfigManager getInstance() { - if (instance == null) - instance = new ConfigManager(); - boolean needCached; - synchronized (instance.cacheHandler) { - needCached = instance.lastModuleCacheTime == 0 || instance.lastScopeCacheTime == 0; - } - if (needCached) { - if (PackageService.isAlive() && UserService.isAlive()) { - Log.d(TAG, "pm & um are ready, updating cache"); - // must ensure cache is valid for later usage - instance.updateCaches(true); - instance.updateManager(false); - } - } - return instance; - } - - private ConfigManager() { - HandlerThread cacheThread = new HandlerThread("cache"); - cacheThread.start(); - cacheHandler = new Handler(cacheThread.getLooper()); - - initDB(); - updateConfig(); - // must ensure cache is valid for later usage - updateCaches(true); - } - - - private T executeInTransaction(Supplier execution) { - try { - db.beginTransaction(); - var res = execution.get(); - db.setTransactionSuccessful(); - return res; - } finally { - db.endTransaction(); - } - } - - private void executeInTransaction(Runnable execution) { - executeInTransaction((Supplier) () -> { - execution.run(); - return null; - }); - } - - private void initDB() { - db.setForeignKeyConstraintsEnabled(true); - int oldVersion = db.getVersion(); - if (oldVersion >= 4) { - // Database is already up to date. - return; - } - - Log.i(TAG, "Initializing/Upgrading database from version " + oldVersion + " to 4"); - db.beginTransaction(); - try { - if (oldVersion == 0) { - db.execSQL(CREATE_MODULES_TABLE); - db.execSQL(CREATE_SCOPE_TABLE); - db.execSQL(CREATE_CONFIG_TABLE); - - var values = new ContentValues(); - values.put("module_pkg_name", "lspd"); - values.put("apk_path", ConfigFileManager.managerApkPath.toString()); - db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); - oldVersion = 1; - } - if (oldVersion < 2) { - // Upgrade from 1 to 2: Recreate tables to enforce constraints and clean up. - db.compileStatement("DROP INDEX IF EXISTS configs_idx;").execute(); - db.compileStatement("DROP TABLE IF EXISTS config;").execute(); - db.compileStatement("ALTER TABLE scope RENAME TO old_scope;").execute(); - db.compileStatement("ALTER TABLE configs RENAME TO old_configs;").execute(); - - db.execSQL(CREATE_SCOPE_TABLE); - db.execSQL(CREATE_CONFIG_TABLE); - - try { - db.compileStatement("INSERT INTO scope SELECT * FROM old_scope;").execute(); - } catch (Throwable e) { - Log.w(TAG, "Failed to migrate scope data", e); - } - try { - db.compileStatement("INSERT INTO configs SELECT * FROM old_configs;").execute(); - } catch (Throwable e) { - Log.w(TAG, "Failed to migrate config data", e); - } - - db.compileStatement("DROP TABLE old_scope;").execute(); - db.compileStatement("DROP TABLE old_configs;").execute(); - db.compileStatement("CREATE INDEX IF NOT EXISTS configs_idx ON configs (module_pkg_name, user_id);").execute(); - } - if (oldVersion < 3) { - // Upgrade from 2 to 3: Rename 'android' scope to 'system'. - db.compileStatement("UPDATE scope SET app_pkg_name = 'system' WHERE app_pkg_name = 'android';").execute(); - } - if (oldVersion < 4) { - // Upgrade from 3 to 4: Add the 'auto_include' column to the modules table. - try { - db.compileStatement("ALTER TABLE modules ADD COLUMN auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1));").execute(); - } catch (SQLiteException ex) { - // This might happen if the column already exists from a previous buggy run. - Log.w(TAG, "Could not add auto_include column, it may already exist.", ex); - } - } - db.setVersion(4); - db.setTransactionSuccessful(); - Log.i(TAG, "Database upgrade to version 4 successful."); - } catch (Throwable e) { - Log.e(TAG, "Failed to initialize or upgrade database, transaction rolled back.", e); - } finally { - db.endTransaction(); - } - } - - private List getAssociatedProcesses(Application app) throws RemoteException { - Pair, Integer> result = PackageService.fetchProcessesWithUid(app); - List processes = new ArrayList<>(); - if (app.packageName.equals("android")) { - // this is hardcoded for ResolverActivity - processes.add(new ProcessScope("system:ui", Process.SYSTEM_UID)); - } - for (String processName : result.first) { - var uid = result.second; - if (uid == Process.SYSTEM_UID && processName.equals("system")) { - // code run in system_server - continue; - } - processes.add(new ProcessScope(processName, uid)); - } - return processes; - } - - private @NonNull - Map> fetchModuleConfig(String name, int user_id) { - var config = new ConcurrentHashMap>(); - - try (Cursor cursor = db.query("configs", new String[]{"`group`", "`key`", "data"}, - "module_pkg_name = ? and user_id = ?", new String[]{name, String.valueOf(user_id)}, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return config; - } - int groupIdx = cursor.getColumnIndex("group"); - int keyIdx = cursor.getColumnIndex("key"); - int dataIdx = cursor.getColumnIndex("data"); - while (cursor.moveToNext()) { - var group = cursor.getString(groupIdx); - var key = cursor.getString(keyIdx); - var data = cursor.getBlob(dataIdx); - var object = SerializationUtilsX.deserialize(data); - if (object == null) continue; - config.computeIfAbsent(group, g -> new HashMap<>()).put(key, object); - } - } - return config; - } - - public void updateModulePrefs(String moduleName, int userId, String group, String key, Object value) { - Map values = new HashMap<>(); - values.put(key, value); - updateModulePrefs(moduleName, userId, group, values); - } - - public void updateModulePrefs(String moduleName, int userId, String group, Map values) { - var config = cachedConfig.computeIfAbsent(new Pair<>(moduleName, userId), module -> fetchModuleConfig(module.first, module.second)); - config.compute(group, (g, prefs) -> { - HashMap newPrefs = prefs == null ? new HashMap<>() : new HashMap<>(prefs); - executeInTransaction(() -> { - for (var entry : values.entrySet()) { - var key = entry.getKey(); - var value = entry.getValue(); - if (value instanceof Serializable) { - newPrefs.put(key, value); - var contents = new ContentValues(); - contents.put("`group`", group); - contents.put("`key`", key); - contents.put("data", SerializationUtilsX.serialize((Serializable) value)); - contents.put("module_pkg_name", moduleName); - contents.put("user_id", String.valueOf(userId)); - db.insertWithOnConflict("configs", null, contents, SQLiteDatabase.CONFLICT_REPLACE); - } else { - newPrefs.remove(key); - db.delete("configs", "module_pkg_name=? and user_id=? and `group`=? and `key`=?", new String[]{moduleName, String.valueOf(userId), group, key}); - } - } - var bundle = new Bundle(); - bundle.putSerializable("config", (Serializable) config); - if (bundle.size() > 1024 * 1024) { - throw new IllegalArgumentException("Preference too large"); - } - }); - return newPrefs; - }); - } - - public void deleteModulePrefs(String moduleName, int userId, String group) { - db.delete("configs", "module_pkg_name=? and user_id=? and `group`=?", new String[]{moduleName, String.valueOf(userId), group}); - var config = cachedConfig.getOrDefault(new Pair<>(moduleName, userId), null); - if (config != null) { - config.remove(group); - } - } - - public HashMap getModulePrefs(String moduleName, int userId, String group) { - var config = cachedConfig.computeIfAbsent(new Pair<>(moduleName, userId), module -> fetchModuleConfig(module.first, module.second)); - return config.getOrDefault(group, new HashMap<>()); - } - - private synchronized void clearCache() { - synchronized (cacheHandler) { - lastScopeCacheTime = 0; - lastModuleCacheTime = 0; - } - cachedModule.clear(); - cachedScope.clear(); - } - - private synchronized void cacheModules() { - // skip caching when pm is not yet available - if (!PackageService.isAlive() || !UserService.isAlive()) return; - synchronized (cacheHandler) { - if (lastModuleCacheTime >= requestModuleCacheTime) return; - else lastModuleCacheTime = SystemClock.elapsedRealtime(); - } - Set toClose = ConcurrentHashMap.newKeySet(); - try (Cursor cursor = db.query(true, "modules", new String[]{"module_pkg_name", "apk_path"}, - "enabled = 1", null, null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return; - } - int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int apkPathIdx = cursor.getColumnIndex("apk_path"); - Set obsoleteModules = ConcurrentHashMap.newKeySet(); - // packageName, apkPath - Map obsoletePaths = new ConcurrentHashMap<>(); - cachedModule.values().removeIf(m -> { - if (m.apkPath == null || !existsInGlobalNamespace(m.apkPath)) { - toClose.addAll(m.file.preLoadedDexes); - return true; - } - return false; - }); - List modules = new ArrayList<>(); - while (cursor.moveToNext()) { - String packageName = cursor.getString(pkgNameIdx); - String apkPath = cursor.getString(apkPathIdx); - if (packageName.equals("lspd")) continue; - var module = new Module(); - module.packageName = packageName; - module.apkPath = apkPath; - modules.add(module); - } - - modules.stream().parallel().filter(m -> { - var oldModule = cachedModule.get(m.packageName); - PackageInfo pkgInfo = null; - try { - pkgInfo = PackageService.getPackageInfoFromAllUsers(m.packageName, MATCH_ALL_FLAGS).values().stream().findFirst().orElse(null); - } catch (Throwable e) { - Log.w(TAG, "Get package info of " + m.packageName, e); - } - if (pkgInfo == null || pkgInfo.applicationInfo == null) { - Log.w(TAG, "Failed to find package info of " + m.packageName); - obsoleteModules.add(m.packageName); - return false; - } - - if (oldModule != null && - pkgInfo.applicationInfo.sourceDir != null && - m.apkPath != null && oldModule.apkPath != null && - existsInGlobalNamespace(m.apkPath) && - Objects.equals(m.apkPath, oldModule.apkPath) && - Objects.equals(new File(pkgInfo.applicationInfo.sourceDir).getParent(), new File(m.apkPath).getParent())) { - if (oldModule.appId != -1) { - Log.d(TAG, m.packageName + " did not change, skip caching it"); - } else { - // cache from system server, update application info - oldModule.applicationInfo = pkgInfo.applicationInfo; - } - return false; - } - m.apkPath = getModuleApkPath(pkgInfo.applicationInfo); - if (m.apkPath == null) { - Log.w(TAG, "Failed to find path of " + m.packageName); - obsoleteModules.add(m.packageName); - return false; - } else { - obsoletePaths.put(m.packageName, m.apkPath); - } - m.appId = pkgInfo.applicationInfo.uid; - m.applicationInfo = pkgInfo.applicationInfo; - m.service = oldModule != null ? oldModule.service : new LSPInjectedModuleService(m.packageName); - return true; - }).forEach(m -> { - var file = ConfigFileManager.loadModule(m.apkPath, dexObfuscate); - if (file == null) { - Log.w(TAG, "failed to load module " + m.packageName); - obsoleteModules.add(m.packageName); - return; - } - m.file = file; - cachedModule.put(m.packageName, m); - }); - - if (PackageService.isAlive()) { - obsoleteModules.forEach(this::removeModuleWithoutCache); - obsoletePaths.forEach((packageName, path) -> updateModuleApkPath(packageName, path, true)); - } else { - Log.w(TAG, "pm is dead while caching. invalidating..."); - clearCache(); - return; - } - } - Log.d(TAG, "cached modules"); - for (var module : cachedModule.entrySet()) { - Log.d(TAG, module.getKey() + " " + module.getValue().apkPath); - } - cacheScopes(); - toClose.forEach(SharedMemory::close); - } - - private synchronized void cacheScopes() { - // skip caching when pm is not yet available - if (!PackageService.isAlive()) return; - synchronized (cacheHandler) { - if (lastScopeCacheTime >= requestScopeCacheTime) return; - else lastScopeCacheTime = SystemClock.elapsedRealtime(); - } - cachedScope.clear(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "module_pkg_name", "user_id"}, - "enabled = 1", null, null, null, null)) { - int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); - int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int userIdIdx = cursor.getColumnIndex("user_id"); - - final var obsoletePackages = new HashSet(); - final var obsoleteModules = new HashSet(); - final var moduleAvailability = new HashMap, Boolean>(); - final var cachedProcessScope = new HashMap, List>(); - - final var denylist = new HashSet<>(getDenyListPackages()); - while (cursor.moveToNext()) { - Application app = new Application(); - app.packageName = cursor.getString(appPkgNameIdx); - app.userId = cursor.getInt(userIdIdx); - var modulePackageName = cursor.getString(modulePkgNameIdx); - - // check if module is present in this user - if (!moduleAvailability.computeIfAbsent(new Pair<>(modulePackageName, app.userId), n -> { - var available = false; - try { - available = PackageService.isPackageAvailable(n.first, n.second, true) && cachedModule.containsKey(modulePackageName); - } catch (Throwable e) { - Log.w(TAG, "check package availability ", e); - } - if (!available) { - var obsoleteModule = new Application(); - obsoleteModule.packageName = modulePackageName; - obsoleteModule.userId = app.userId; - obsoleteModules.add(obsoleteModule); - } - return available; - })) continue; - - // system server always loads database - if (app.packageName.equals("system")) continue; - - try { - List processesScope = cachedProcessScope.computeIfAbsent(new Pair<>(app.packageName, app.userId), (k) -> { - try { - if (denylist.contains(app.packageName)) - Log.w(TAG, app.packageName + " is on denylist. It may not take effect."); - return getAssociatedProcesses(app); - } catch (RemoteException e) { - return Collections.emptyList(); - } - }); - if (processesScope.isEmpty()) { - obsoletePackages.add(app); - continue; - } - var module = cachedModule.get(modulePackageName); - assert module != null; - for (ProcessScope processScope : processesScope) { - cachedScope.computeIfAbsent(processScope, - ignored -> new LinkedList<>()).add(module); - // Always allow the module to inject itself - if (modulePackageName.equals(app.packageName)) { - var appId = processScope.uid % PER_USER_RANGE; - for (var user : UserService.getUsers()) { - var moduleUid = user.id * PER_USER_RANGE + appId; - if (moduleUid == processScope.uid) continue; // skip duplicate - var moduleSelf = new ProcessScope(processScope.processName, moduleUid); - cachedScope.computeIfAbsent(moduleSelf, - ignored -> new LinkedList<>()).add(module); - } - } - } - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - if (PackageService.isAlive()) { - for (Application obsoletePackage : obsoletePackages) { - Log.d(TAG, "removing obsolete package: " + obsoletePackage.packageName + "/" + obsoletePackage.userId); - removeAppWithoutCache(obsoletePackage); - } - for (Application obsoleteModule : obsoleteModules) { - Log.d(TAG, "removing obsolete module: " + obsoleteModule.packageName + "/" + obsoleteModule.userId); - removeModuleScopeWithoutCache(obsoleteModule); - removeBlockedScopeRequest(obsoleteModule.packageName); - } - } else { - Log.w(TAG, "pm is dead while caching. invalidating..."); - clearCache(); - return; - } - } - Log.d(TAG, "cached scope"); - cachedScope.forEach((ps, modules) -> { - Log.d(TAG, ps.processName + "/" + ps.uid); - modules.forEach(module -> Log.d(TAG, "\t" + module.packageName)); - }); - } - - // This is called when a new process created, use the cached result - public List getModulesForProcess(String processName, int uid) { - return isManager(uid) ? Collections.emptyList() : cachedScope.getOrDefault(new ProcessScope(processName, uid), Collections.emptyList()); - } - - // This is called when a new process created, use the cached result - public boolean shouldSkipProcess(ProcessScope scope) { - return !cachedScope.containsKey(scope) && !isManager(scope.uid); - } - - public boolean isUidHooked(int uid) { - return cachedScope.keySet().stream().reduce(false, (p, scope) -> p || scope.uid == uid, Boolean::logicalOr); - } - - @Nullable - public List getModuleScope(String packageName) { - if (packageName.equals("lspd")) return null; - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "user_id"}, - "modules.module_pkg_name = ?", new String[]{packageName}, null, null, null)) { - if (cursor == null) { - return null; - } - int userIdIdx = cursor.getColumnIndex("user_id"); - int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); - ArrayList result = new ArrayList<>(); - while (cursor.moveToNext()) { - Application scope = new Application(); - scope.packageName = cursor.getString(appPkgNameIdx); - scope.userId = cursor.getInt(userIdIdx); - result.add(scope); - } - return result; - } - } - - @Nullable - public String getModuleApkPath(ApplicationInfo info) { - String[] apks; - if (info.splitSourceDirs != null) { - apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); - apks[info.splitSourceDirs.length] = info.sourceDir; - } else apks = new String[]{info.sourceDir}; - var apkPath = Arrays.stream(apks).parallel().filter(apk -> { - if (apk == null) { - Log.w(TAG, info.packageName + " has null apk path???"); - return false; - } - try (var zip = new ZipFile(toGlobalNamespace(apk))) { - return zip.getEntry("META-INF/xposed/java_init.list") != null || zip.getEntry("assets/xposed_init") != null; - } catch (IOException e) { - return false; - } - }).findFirst(); - return apkPath.orElse(null); - } - - public boolean updateModuleApkPath(String packageName, String apkPath, boolean force) { - if (apkPath == null || packageName.equals("lspd")) return false; - if (db.inTransaction()) { - Log.w(TAG, "update module apk path should not be called inside transaction"); - return false; - } - - ContentValues values = new ContentValues(); - values.put("module_pkg_name", packageName); - values.put("apk_path", apkPath); - // insert or update in two step since insert or replace will change the autoincrement mid - int count = (int) db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); - if (count < 0) { - var cached = cachedModule.getOrDefault(packageName, null); - if (force || cached == null || cached.apkPath == null || !cached.apkPath.equals(apkPath)) - count = db.updateWithOnConflict("modules", values, "module_pkg_name=?", new String[]{packageName}, SQLiteDatabase.CONFLICT_IGNORE); - else - count = 0; - } - // force update is because cache is already update to date - // skip caching again - if (!force && count > 0) { - // Called by oneway binder - updateCaches(true); - return true; - } - return count > 0; - } - - // Only be called before updating modules. No need to cache. - private int getModuleId(String packageName) { - if (packageName.equals("lspd")) return -1; - if (db.inTransaction()) { - Log.w(TAG, "get module id should not be called inside transaction"); - return -1; - } - try (Cursor cursor = db.query("modules", new String[]{"mid"}, "module_pkg_name=?", new String[]{packageName}, null, null, null)) { - if (cursor == null) return -1; - if (cursor.getCount() != 1) return -1; - cursor.moveToFirst(); - return cursor.getInt(cursor.getColumnIndexOrThrow("mid")); - } - } - - public boolean setModuleScope(String packageName, List scopes) throws RemoteException { - if (scopes == null) return false; - enableModule(packageName); - int mid = getModuleId(packageName); - if (mid == -1) return false; - executeInTransaction(() -> { - db.delete("scope", "mid = ?", new String[]{String.valueOf(mid)}); - for (Application app : scopes) { - if (app.packageName.equals("system") && app.userId != 0) continue; - ContentValues values = new ContentValues(); - values.put("mid", mid); - values.put("app_pkg_name", app.packageName); - values.put("user_id", app.userId); - db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE); - } - }); - // Called by manager, should be async - updateCaches(false); - return true; - } - - public boolean setModuleScope(String packageName, String scopePackageName, int userId) { - if (scopePackageName == null) return false; - int mid = getModuleId(packageName); - if (mid == -1) return false; - if (scopePackageName.equals("system") && userId != 0) return false; - executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("mid", mid); - values.put("app_pkg_name", scopePackageName); - values.put("user_id", userId); - db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE); - }); - // Called by xposed service, should be async - updateCaches(false); - return true; - } - - public boolean removeModuleScope(String packageName, String scopePackageName, int userId) { - if (scopePackageName == null) return false; - int mid = getModuleId(packageName); - if (mid == -1) return false; - if (scopePackageName.equals("system") && userId != 0) return false; - executeInTransaction(() -> { - db.delete("scope", "mid = ? AND app_pkg_name = ? AND user_id = ?", new String[]{String.valueOf(mid), scopePackageName, String.valueOf(userId)}); - }); - // Called by xposed service, should be async - updateCaches(false); - return true; - } - - - public String[] enabledModules() { - return listModules("enabled"); - } - - public boolean removeModule(String packageName) { - if (removeModuleWithoutCache(packageName)) { - // called by oneway binder - // Called only when the application is completely uninstalled - // If it's a module we need to return as soon as possible to broadcast to the manager - // for updating the module status - updateCaches(false); - return true; - } - return false; - } - - private boolean removeModuleWithoutCache(String packageName) { - if (packageName.equals("lspd")) return false; - boolean res = executeInTransaction(() -> db.delete("modules", "module_pkg_name = ?", new String[]{packageName}) > 0); - try { - for (var user : UserService.getUsers()) { - removeModulePrefs(user.id, packageName); - } - } catch (Throwable e) { - Log.w(TAG, "remove module prefs for " + packageName); - } - return res; - } - - private boolean removeModuleScopeWithoutCache(Application module) { - if (module.packageName.equals("lspd")) return false; - int mid = getModuleId(module.packageName); - if (mid == -1) return false; - boolean res = executeInTransaction(() -> db.delete("scope", "mid = ? and user_id = ?", new String[]{String.valueOf(mid), String.valueOf(module.userId)}) > 0); - try { - removeModulePrefs(module.userId, module.packageName); - } catch (IOException e) { - Log.w(TAG, "removeModulePrefs", e); - } - return res; - } - - private boolean removeAppWithoutCache(Application app) { - return executeInTransaction(() -> db.delete("scope", "app_pkg_name = ? AND user_id=?", - new String[]{app.packageName, String.valueOf(app.userId)}) > 0); - } - - public boolean disableModule(String packageName) { - if (packageName.equals("lspd")) return false; - boolean changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("enabled", 0); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }); - if (changed) { - // called by manager, should be async - updateCaches(false); - return true; - } else { - return false; - } - } - - public boolean enableModule(String packageName) throws RemoteException { - if (packageName.equals("lspd")) return false; - PackageInfo pkgInfo = PackageService.getPackageInfoFromAllUsers(packageName, PackageService.MATCH_ALL_FLAGS).values().stream().findFirst().orElse(null); - if (pkgInfo == null || pkgInfo.applicationInfo == null) return false; - var modulePath = getModuleApkPath(pkgInfo.applicationInfo); - if (modulePath == null) return false; - boolean changed = updateModuleApkPath(packageName, modulePath, false); - changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("enabled", 1); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }) || changed; - if (changed) { - // Called by manager, should be async - updateCaches(false); - return true; - } else { - return false; - } - } - - public void updateCache() { - // Called by oneway binder - updateCaches(true); - } - - public void updateAppCache() { - // Called by oneway binder - cacheScopes(); - } - - public void setVerboseLog(boolean on) { - if (BuildConfig.DEBUG) return; - var logcatService = ServiceManager.getLogcatService(); - if (on) { - logcatService.startVerbose(); - } else { - logcatService.stopVerbose(); - } - updateModulePrefs("lspd", 0, "config", "enable_verbose_log", on); - verboseLog = on; - } - - public boolean verboseLog() { - return BuildConfig.DEBUG || verboseLog; - } - - public void setLogWatchdog(boolean on) { - var logcatService = ServiceManager.getLogcatService(); - if (on) { - logcatService.enableWatchdog(); - } else { - logcatService.disableWatchdog(); - } - updateModulePrefs("lspd", 0, "config", "enable_log_watchdog", on); - logWatchdog = on; - } - - public boolean isLogWatchdogEnabled() { - return logWatchdog; - } - - public void setDexObfuscate(boolean on) { - updateModulePrefs("lspd", 0, "config", "enable_dex_obfuscate", on); - } - - public boolean scopeRequestBlocked(String packageName) { - return scopeRequestBlocked.contains(packageName); - } - - public void blockScopeRequest(String packageName) { - var set = new HashSet<>(scopeRequestBlocked); - set.add(packageName); - updateModulePrefs("lspd", 0, "config", "scope_request_blocked", set); - scopeRequestBlocked = set; - } - - public void removeBlockedScopeRequest(String packageName) { - var set = new HashSet<>(scopeRequestBlocked); - set.remove(packageName); - updateModulePrefs("lspd", 0, "config", "scope_request_blocked", set); - scopeRequestBlocked = set; - } - - // this is for manager and should not use the cache result - boolean dexObfuscate() { - var bool = getModulePrefs("lspd", 0, "config").get("enable_dex_obfuscate"); - return bool == null || (boolean) bool; - } - - public boolean enableStatusNotification() { - Log.d(TAG, "show status notification = " + enableStatusNotification); - return enableStatusNotification; - } - - public void setEnableStatusNotification(boolean enable) { - updateModulePrefs("lspd", 0, "config", "enable_status_notification", enable); - enableStatusNotification = enable; - } - - public ParcelFileDescriptor getManagerApk() { - try { - return ConfigFileManager.getManagerApk(); - } catch (Throwable e) { - Log.e(TAG, "failed to open manager apk", e); - return null; - } - } - - public ParcelFileDescriptor getModulesLog() { - try { - var modulesLog = ServiceManager.getLogcatService().getModulesLog(); - if (modulesLog == null) return null; - return ParcelFileDescriptor.open(modulesLog, ParcelFileDescriptor.MODE_READ_ONLY); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - - public ParcelFileDescriptor getVerboseLog() { - try { - var verboseLog = ServiceManager.getLogcatService().getVerboseLog(); - if (verboseLog == null) return null; - return ParcelFileDescriptor.open(verboseLog, ParcelFileDescriptor.MODE_READ_ONLY); - } catch (FileNotFoundException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - - public boolean clearLogs(boolean verbose) { - ServiceManager.getLogcatService().refresh(verbose); - return true; - } - - public boolean isManager(int uid) { - return uid == managerUid; - } - - public boolean isManagerInstalled() { - return managerUid != -1; - } - - public String getPrefsPath(String packageName, int uid) { - int userId = uid / PER_USER_RANGE; - var path = miscPath.resolve("prefs" + (userId == 0 ? "" : String.valueOf(userId))).resolve(packageName); - var module = cachedModule.getOrDefault(packageName, null); - if (module != null && module.appId == uid % PER_USER_RANGE) { - try { - var perms = PosixFilePermissions.fromString("rwx--x--x"); - Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); - walkFileTree(path, p -> { - try { - Os.chown(p.toString(), uid, uid); - } catch (ErrnoException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - }); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return path.toString(); - } - - // this is slow, avoid using it - public Module getModule(int uid) { - for (var module : cachedModule.values()) { - if (module.appId == uid % PER_USER_RANGE) return module; - } - return null; - } - - private void walkFileTree(Path rootDir, Consumer action) throws IOException { - if (Files.notExists(rootDir)) return; - Files.walkFileTree(rootDir, new SimpleFileVisitor<>() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - action.accept(dir); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - action.accept(file); - return FileVisitResult.CONTINUE; - } - }); - } - - private void removeModulePrefs(int uid, String packageName) throws IOException { - if (packageName == null) return; - var path = Paths.get(getPrefsPath(packageName, uid)); - ConfigFileManager.deleteFolderIfExists(path); - } - - public List getDenyListPackages() { - List result = new ArrayList<>(); - if (!getApi().equals("Zygisk")) return result; - if (!ConfigFileManager.magiskDbPath.exists()) return result; - try (final SQLiteDatabase magiskDb = - SQLiteDatabase.openDatabase(ConfigFileManager.magiskDbPath, new SQLiteDatabase.OpenParams.Builder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build())) { - try (Cursor cursor = magiskDb.query("settings", new String[]{"value"}, "`key`=?", new String[]{"denylist"}, null, null, null)) { - if (!cursor.moveToNext()) return result; - int valueIndex = cursor.getColumnIndex("value"); - if (valueIndex >= 0 && cursor.getInt(valueIndex) == 0) return result; - } - try (Cursor cursor = magiskDb.query(true, "denylist", new String[]{"package_name"}, null, null, null, null, null, null, null)) { - if (cursor == null) return result; - int packageNameIdx = cursor.getColumnIndex("package_name"); - while (cursor.moveToNext()) { - result.add(cursor.getString(packageNameIdx)); - } - return result; - } - } catch (Throwable e) { - Log.e(TAG, "get denylist", e); - } - return result; - } - - public void setApi(String api) { - this.api = api; - } - - public String getApi() { - return api; - } - - public void exportScopes(ZipOutputStream os) throws IOException { - os.putNextEntry(new ZipEntry("scopes.txt")); - cachedScope.forEach((scope, modules) -> { - try { - os.write((scope.processName + "/" + scope.uid + "\n").getBytes(StandardCharsets.UTF_8)); - for (var module : modules) { - os.write(("\t" + module.packageName + "\n").getBytes(StandardCharsets.UTF_8)); - for (var cn : module.file.moduleClassNames) { - os.write(("\t\t" + cn + "\n").getBytes(StandardCharsets.UTF_8)); - } - for (var ln : module.file.moduleLibraryNames) { - os.write(("\t\t" + ln + "\n").getBytes(StandardCharsets.UTF_8)); - } - } - } catch (IOException e) { - Log.w(TAG, scope.processName, e); - } - }); - os.closeEntry(); - } - - synchronized SharedMemory getPreloadDex() { - return ConfigFileManager.getPreloadDex(dexObfuscate); - } - - public boolean getAutoInclude(String packageName) { - try (Cursor cursor = db.query("modules", new String[]{"auto_include"}, - "module_pkg_name = ? and auto_include = 1", new String[]{packageName}, null, null, null, null)) { - return cursor == null || cursor.moveToNext(); - } - } - - public boolean setAutoInclude(String packageName, boolean enable) { - boolean changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("auto_include", enable ? 1 : 0); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }); - return true; - } - - public String[] getAutoIncludeModules() { - return listModules("auto_include"); - } - - private String[] listModules(String column) { - try (Cursor cursor = db.query("modules", new String[]{"module_pkg_name"}, column + " = 1", null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "query " + column + " modules failed"); - return null; - } - int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - HashSet result = new HashSet<>(); - while (cursor.moveToNext()) { - var pkgName = cursor.getString(modulePkgNameIdx); - if (pkgName.equals("lspd")) continue; - result.add(pkgName); - } - return result.toArray(new String[0]); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java b/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java deleted file mode 100644 index f4da23ebb..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_CRASHED; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_MOUNT_FAILED; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_OK; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT; - -import android.net.LocalServerSocket; -import android.os.Build; -import android.os.FileObserver; -import android.os.Process; -import android.os.SELinux; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; - -@RequiresApi(Build.VERSION_CODES.Q) -public class Dex2OatService implements Runnable { - private static final String TAG = "LSPosedDex2Oat"; - private static final String WRAPPER32 = "bin/dex2oat32"; - private static final String WRAPPER64 = "bin/dex2oat64"; - private static final String HOOKER32 = "bin/liboat_hook32.so"; - private static final String HOOKER64 = "bin/liboat_hook64.so"; - - private final String[] dex2oatArray = new String[6]; - private final FileDescriptor[] fdArray = new FileDescriptor[6]; - private final FileObserver selinuxObserver; - private int compatibility = DEX2OAT_OK; - - private void openDex2oat(int id, String path) { - try { - var fd = Os.open(path, OsConstants.O_RDONLY, 0); - dex2oatArray[id] = path; - fdArray[id] = fd; - } catch (ErrnoException ignored) { - } - } - - /** - * Checks the ELF header of the target file. - * If 32-bit -> Assigns to Index 0 (Release) or 1 (Debug). - * If 64-bit -> Assigns to Index 2 (Release) or 3 (Debug). - */ - private void checkAndAddDex2Oat(String path) { - if (path == null) - return; - File file = new File(path); - if (!file.exists()) - return; - - try (FileInputStream fis = new FileInputStream(file)) { - byte[] header = new byte[5]; - if (fis.read(header) != 5) - return; - - // 1. Verify ELF Magic: 0x7F 'E' 'L' 'F' - if (header[0] != 0x7F || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') { - return; - } - - // 2. Check Architecture (header[4]): 1 = 32-bit, 2 = 64-bit - boolean is32Bit = (header[4] == 1); - boolean is64Bit = (header[4] == 2); - boolean isDebug = path.contains("dex2oatd"); - - int index = -1; - - if (is32Bit) { - index = isDebug ? 1 : 0; // Index 0/1 maps to r32/d32 in C++ - } else if (is64Bit) { - index = isDebug ? 3 : 2; // Index 2/3 maps to r64/d64 in C++ - } - - // 3. Assign to the detected slot - if (index != -1 && dex2oatArray[index] == null) { - dex2oatArray[index] = path; - try { - // Open the FD for the wrapper to use later - fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0); - Log.i(TAG, "Detected " + path + " as " + (is64Bit ? "64-bit" : "32-bit") + " -> Assigned Index " - + index); - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open FD for " + path, e); - dex2oatArray[index] = null; - } - } - } catch (IOException e) { - // File not readable, skip - } - } - - public Dex2OatService() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - // Android 10: Check the standard path. - // Logic will detect if it is 32-bit and put it in Index 0. - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat"); - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd"); - - // Check for explicit 64-bit paths (just in case) - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64"); - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64"); - } else { - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64"); - } - - openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so"); - openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so"); - - var enforce = Paths.get("/sys/fs/selinux/enforce"); - var policy = Paths.get("/sys/fs/selinux/policy"); - var list = new ArrayList(); - list.add(enforce.toFile()); - list.add(policy.toFile()); - selinuxObserver = new FileObserver(list, FileObserver.CLOSE_WRITE) { - @Override - public synchronized void onEvent(int i, @Nullable String s) { - Log.d(TAG, "SELinux status changed"); - if (compatibility == DEX2OAT_CRASHED) { - stopWatching(); - return; - } - - boolean enforcing = false; - try (var is = Files.newInputStream(enforce)) { - enforcing = is.read() == '1'; - } catch (IOException ignored) { - } - - if (!enforcing) { - if (compatibility == DEX2OAT_OK) doMount(false); - compatibility = DEX2OAT_SELINUX_PERMISSIVE; - } else if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", - "u:object_r:dex2oat_exec:s0", "file", "execute") - || SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", - "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans")) { - if (compatibility == DEX2OAT_OK) doMount(false); - compatibility = DEX2OAT_SEPOLICY_INCORRECT; - } else if (compatibility != DEX2OAT_OK) { - doMount(true); - if (notMounted()) { - doMount(false); - compatibility = DEX2OAT_MOUNT_FAILED; - stopWatching(); - } else { - compatibility = DEX2OAT_OK; - } - } - } - - @Override - public void stopWatching() { - super.stopWatching(); - Log.w(TAG, "SELinux observer stopped"); - } - }; - } - - private boolean notMounted() { - for (int i = 0; i < dex2oatArray.length && i < 4; i++) { - var bin = dex2oatArray[i]; - if (bin == null) continue; - try { - var apex = Os.stat("/proc/1/root" + bin); - var wrapper = Os.stat(i < 2 ? WRAPPER32 : WRAPPER64); - if (apex.st_dev != wrapper.st_dev || apex.st_ino != wrapper.st_ino) { - Log.w(TAG, "Check mount failed for " + bin); - return true; - } - } catch (ErrnoException e) { - Log.e(TAG, "Check mount failed for " + bin, e); - return true; - } - } - Log.d(TAG, "Check mount succeeded"); - return false; - } - - private void doMount(boolean enabled) { - doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]); - } - - public void start() { - if (notMounted()) { // Already mounted when restart daemon - doMount(true); - if (notMounted()) { - doMount(false); - compatibility = DEX2OAT_MOUNT_FAILED; - return; - } - } - - var thread = new Thread(this); - thread.setName("dex2oat"); - thread.start(); - selinuxObserver.startWatching(); - selinuxObserver.onEvent(0, null); - } - - @Override - public void run() { - Log.i(TAG, "Dex2oat wrapper daemon start"); - var sockPath = getSockPath(); - Log.d(TAG, "wrapper path: " + sockPath); - var xposed_file = "u:object_r:xposed_file:s0"; - var dex2oat_exec = "u:object_r:dex2oat_exec:s0"; - if (SELinux.checkSELinuxAccess("u:r:dex2oat:s0", dex2oat_exec, - "file", "execute_no_trans")) { - SELinux.setFileContext(WRAPPER32, dex2oat_exec); - SELinux.setFileContext(WRAPPER64, dex2oat_exec); - setSockCreateContext("u:r:dex2oat:s0"); - } else { - SELinux.setFileContext(WRAPPER32, xposed_file); - SELinux.setFileContext(WRAPPER64, xposed_file); - setSockCreateContext("u:r:installd:s0"); - } - SELinux.setFileContext(HOOKER32, xposed_file); - SELinux.setFileContext(HOOKER64, xposed_file); - try (var server = new LocalServerSocket(sockPath)) { - setSockCreateContext(null); - while (true) { - try (var client = server.accept(); - var is = client.getInputStream(); - var os = client.getOutputStream()) { - var id = is.read(); - var fd = new FileDescriptor[]{fdArray[id]}; - client.setFileDescriptorsForSend(fd); - os.write(1); - Log.d(TAG, "Sent fd of " + dex2oatArray[id]); - } - } - } catch (IOException e) { - Log.e(TAG, "Dex2oat wrapper daemon crashed", e); - if (compatibility == DEX2OAT_OK) { - doMount(false); - compatibility = DEX2OAT_CRASHED; - } - } - } - - public int getCompatibility() { - return compatibility; - } - - private native void doMountNative(boolean enabled, - String r32, String d32, String r64, String d64); - - private static native boolean setSockCreateContext(String context); - - private native String getSockPath(); -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java deleted file mode 100644 index f5cd46555..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.os.IBinder; -import android.os.Parcel; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.models.Module; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class LSPApplicationService extends ILSPApplicationService.Stub { - final static int DEX_TRANSACTION_CODE = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; - final static int OBFUSCATION_MAP_TRANSACTION_CODE = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; - // key: - private final static Map, ProcessInfo> processes = new ConcurrentHashMap<>(); - - static class ProcessInfo implements DeathRecipient { - final int uid; - final int pid; - final String processName; - final IBinder heartBeat; - - ProcessInfo(int uid, int pid, String processName, IBinder heartBeat) throws RemoteException { - this.uid = uid; - this.pid = pid; - this.processName = processName; - this.heartBeat = heartBeat; - heartBeat.linkToDeath(this, 0); - Log.d(TAG, "register " + this); - processes.put(new Pair<>(uid, pid), this); - } - - @Override - public void binderDied() { - Log.d(TAG, this + " is dead"); - heartBeat.unlinkToDeath(this, 0); - processes.remove(new Pair<>(uid, pid), this); - } - - @NonNull - @Override - public String toString() { - return "ProcessInfo{" + - "uid=" + uid + - ", pid=" + pid + - ", processName='" + processName + '\'' + - ", heartBeat=" + heartBeat + - '}'; - } - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - Log.d(TAG, "LSPApplicationService.onTransact: code=" + code); - switch (code) { - case DEX_TRANSACTION_CODE: { - var shm = ConfigManager.getInstance().getPreloadDex(); - if (shm == null) return false; - reply.writeNoException(); - // assume that write only a fd - shm.writeToParcel(reply, 0); - reply.writeLong(shm.getSize()); - return true; - } - case OBFUSCATION_MAP_TRANSACTION_CODE: { - var obfuscation = ConfigManager.getInstance().dexObfuscate(); - var signatures = ObfuscationManager.getSignatures(); - reply.writeNoException(); - reply.writeInt(signatures.size() * 2); - for (Map.Entry entry : signatures.entrySet()) { - reply.writeString(entry.getKey()); - // return val = key if obfuscation disabled - reply.writeString(obfuscation ? entry.getValue() : entry.getKey()); - } - return true; - } - } - return super.onTransact(code, data, reply, flags); - } - - public boolean registerHeartBeat(int uid, int pid, String processName, IBinder heartBeat) { - try { - new ProcessInfo(uid, pid, processName, heartBeat); - return true; - } catch (RemoteException e) { - return false; - } - } - - private List getAllModulesList() throws RemoteException { - var processInfo = ensureRegistered(); - if (processInfo.uid == Process.SYSTEM_UID && processInfo.processName.equals("system")) { - return ConfigManager.getInstance().getModulesForSystemServer(); - } - if (ServiceManager.getManagerService().isRunningManager(processInfo.pid, processInfo.uid)) - return Collections.emptyList(); - return ConfigManager.getInstance().getModulesForProcess(processInfo.processName, processInfo.uid); - } - - @Override - public boolean isLogMuted() throws RemoteException { - return !ServiceManager.getManagerService().isVerboseLog(); - } - - @Override - public List getLegacyModulesList() throws RemoteException { - return getAllModulesList().stream().filter(m -> m.file.legacy).collect(Collectors.toList()); - } - - @Override - public List getModulesList() throws RemoteException { - return getAllModulesList().stream().filter(m -> !m.file.legacy).collect(Collectors.toList()); - } - - @Override - public String getPrefsPath(String packageName) throws RemoteException { - ensureRegistered(); - return ConfigManager.getInstance().getPrefsPath(packageName, getCallingUid()); - } - - @Override - public ParcelFileDescriptor requestInjectedManagerBinder(List binder) throws RemoteException { - var processInfo = ensureRegistered(); - if (ServiceManager.getManagerService().postStartManager(processInfo.pid, processInfo.uid) || - ConfigManager.getInstance().isManager(processInfo.uid)) { - binder.add(ServiceManager.getManagerService().obtainManagerBinder(processInfo.heartBeat, processInfo.pid, processInfo.uid)); - } - return ConfigManager.getInstance().getManagerApk(); - } - - public boolean hasRegister(int uid, int pid) { - return processes.containsKey(new Pair<>(uid, pid)); - } - - @NonNull - private ProcessInfo ensureRegistered() throws RemoteException { - var uid = getCallingUid(); - var pid = getCallingPid(); - var key = new Pair<>(uid, pid); - ProcessInfo processInfo = processes.getOrDefault(key, null); - if (processInfo == null || uid != processInfo.uid || pid != processInfo.pid) { - processes.remove(key, processInfo); - Log.w(TAG, "non-authorized: info=" + processInfo + " uid=" + uid + " pid=" + pid); - throw new RemoteException("Not registered"); - } - return processInfo; - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java deleted file mode 100644 index fa402923c..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.LSPModuleService.FILES_DIR; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; - -import android.os.Binder; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.util.Log; - -import org.lsposed.lspd.models.Module; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedService; - -public class LSPInjectedModuleService extends ILSPInjectedModuleService.Stub { - - private static final String TAG = "LSPosedInjectedModuleService"; - - private final String mPackageName; - - Map> callbacks = new ConcurrentHashMap<>(); - - LSPInjectedModuleService(String packageName) { - mPackageName = packageName; - } - - @Override - public long getFrameworkProperties() { - var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; - if (ConfigManager.getInstance().dexObfuscate()) { - prop = prop | IXposedService.PROP_RT_API_PROTECTION; - } - return prop; - } - - @Override - public Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback) { - var bundle = new Bundle(); - var userId = Binder.getCallingUid() / PER_USER_RANGE; - bundle.putSerializable("map", ConfigManager.getInstance().getModulePrefs(mPackageName, userId, group)); - if (callback != null) { - var groupCallbacks = callbacks.computeIfAbsent(group, k -> ConcurrentHashMap.newKeySet()); - groupCallbacks.add(callback); - try { - callback.asBinder().linkToDeath(() -> groupCallbacks.remove(callback), 0); - } catch (RemoteException e) { - Log.w(TAG, "requestRemotePreferences: ", e); - } - } - return bundle; - } - - @Override - public ParcelFileDescriptor openRemoteFile(String path) throws RemoteException { - ConfigFileManager.ensureModuleFilePath(path); - var userId = Binder.getCallingUid() / PER_USER_RANGE; - try { - var dir = ConfigFileManager.resolveModuleDir(mPackageName, FILES_DIR, userId, -1); - return ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_READ_ONLY); - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public String[] getRemoteFileList() throws RemoteException { - var userId = Binder.getCallingUid() / PER_USER_RANGE; - try { - var dir = ConfigFileManager.resolveModuleDir(mPackageName, FILES_DIR, userId, -1); - var files = dir.toFile().list(); - return files == null ? new String[0] : files; - - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - void onUpdateRemotePreferences(String group, Bundle diff) { - var groupCallbacks = callbacks.get(group); - if (groupCallbacks != null) { - for (var callback : groupCallbacks) { - try { - callback.onUpdate(diff); - } catch (RemoteException e) { - groupCallbacks.remove(callback); - } - } - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java deleted file mode 100644 index 0ed6b21f7..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ /dev/null @@ -1,569 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.Context.BIND_AUTO_CREATE; -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.annotation.SuppressLint; -import android.app.IServiceConnection; -import android.content.AttributionSource; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.VersionedPackage; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SystemProperties; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; -import android.view.IWindowManager; - -import androidx.annotation.NonNull; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.ILSPManagerService; -import org.lsposed.lspd.models.Application; -import org.lsposed.lspd.models.UserInfo; -import org.lsposed.lspd.util.Utils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import hidden.HiddenApiBridge; -import io.github.libxposed.service.IXposedService; -import rikka.parcelablelist.ParcelableListSlice; - -public class LSPManagerService extends ILSPManagerService.Stub { - - private static Intent managerIntent = null; - private boolean enabled = true; - - public class ManagerGuard implements IBinder.DeathRecipient { - private final @NonNull - IBinder binder; - private final int pid; - private final int uid; - private final IServiceConnection connection = new IServiceConnection.Stub() { - @Override - public void connected(ComponentName name, IBinder service, boolean dead) { - } - }; - - public ManagerGuard(@NonNull IBinder binder, int pid, int uid) { - guard = this; - this.pid = pid; - this.uid = uid; - this.binder = binder; - try { - this.binder.linkToDeath(this, 0); - if (Utils.isMIUI) { - var intent = new Intent(); - intent.setComponent(ComponentName.unflattenFromString("com.miui.securitycore/com.miui.xspace.service.XSpaceService")); - ActivityManagerService.bindService(intent, intent.getType(), connection, BIND_AUTO_CREATE, "android", 0); - } - } catch (Throwable e) { - Log.e(TAG, "manager guard", e); - guard = null; - } - } - - @Override - public void binderDied() { - try { - binder.unlinkToDeath(this, 0); - ActivityManagerService.unbindService(connection); - } catch (Throwable e) { - Log.e(TAG, "manager guard", e); - } - guard = null; - } - - boolean isAlive() { - return binder.isBinderAlive(); - } - } - - public ManagerGuard guard = null; - - // guard to determine the manager or the injected app - // that is to say, to make the parasitic success, - // we should make sure no extra launch after parasitic - // launch is queued and before the process is started - private boolean pendingManager = false; - private int managerPid = -1; - - LSPManagerService() { - } - - private static Intent getManagerIntent() { - if (managerIntent != null) return managerIntent; - try { - var intent = PackageService.getLaunchIntentForPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - if (intent == null) { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0); - if (pkgInfo != null && pkgInfo.activities != null && pkgInfo.activities.length > 0) { - for (var activityInfo : pkgInfo.activities) { - if (activityInfo.processName.equals(activityInfo.packageName)) { - intent = new Intent(); - intent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); - intent.setAction(Intent.ACTION_MAIN); - break; - } - } - } - } - if (intent != null) { - if (intent.getCategories() != null) intent.getCategories().clear(); - intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER"); - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - managerIntent = new Intent(intent); - } - } catch (RemoteException e) { - Log.e(TAG, "get Intent", e); - } - return managerIntent; - } - - static void openManager(Uri withData) { - var intent = getManagerIntent(); - if (intent == null) return; - intent = new Intent(intent); - intent.setData(withData); - try { - ActivityManagerService.startActivityAsUserWithFeature("android", null, intent, intent.getType(), null, null, 0, 0, null, null, 0); - } catch (RemoteException e) { - Log.e(TAG, "failed to open manager"); - } - } - - @SuppressLint("WrongConstant") - public static void broadcastIntent(Intent inIntent) { - var intent = new Intent("org.lsposed.manager.NOTIFICATION"); - intent.putExtra(Intent.EXTRA_INTENT, inIntent); - intent.addFlags(0x01000000); //Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND - intent.addFlags(0x00400000); //Intent.FLAG_RECEIVER_FROM_SHELL - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - try { - ActivityManagerService.broadcastIntentWithFeature(null, intent, - null, null, 0, null, null, - null, -1, null, true, false, - 0); - intent.setPackage(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME); - ActivityManagerService.broadcastIntentWithFeature(null, intent, - null, null, 0, null, null, - null, -1, null, true, false, - 0); - } catch (RemoteException t) { - Log.e(TAG, "Broadcast to manager failed: ", t); - } - } - - private void ensureWebViewPermission(File f) { - if (!f.exists()) return; - SELinux.setFileContext(f.getAbsolutePath(), "u:object_r:xposed_file:s0"); - try { - Os.chown(f.getAbsolutePath(), BuildConfig.MANAGER_INJECTED_UID, BuildConfig.MANAGER_INJECTED_UID); - } catch (ErrnoException e) { - Log.e(TAG, "chown of webview", e); - } - if (f.isDirectory()) { - for (var g : f.listFiles()) { - ensureWebViewPermission(g); - } - } - } - - private void ensureWebViewPermission() { - try { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, 0, 0); - if (pkgInfo != null) { - var cacheDir = new File(HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(pkgInfo.applicationInfo) + "/cache"); - // The cache directory does not exist after `pm clear` - cacheDir.mkdirs(); - ensureWebViewPermission(cacheDir); - } - } catch (Throwable e) { - Log.w(TAG, "cannot ensure webview dir", e); - } - } - - synchronized boolean preStartManager() { - pendingManager = true; - managerPid = -1; - return true; - } - - // return true to inject manager - synchronized boolean shouldStartManager(int pid, int uid, String processName) { - if (!enabled || uid != BuildConfig.MANAGER_INJECTED_UID || !BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(processName) || !pendingManager) - return false; - pendingManager = false; - managerPid = pid; - Log.d(TAG, "starting injected manager: pid = " + pid + " uid = " + uid + " processName = " + processName); - return true; - } - - synchronized boolean setEnabled(boolean newValue) { - enabled = newValue; - Log.i(TAG, "manager enabled = " + enabled); - return enabled; - } - - // return true to send manager binder - boolean postStartManager(int pid, int uid) { - return enabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid; - } - - public @NonNull - IBinder obtainManagerBinder(@NonNull IBinder heartbeat, int pid, int uid) { - new ManagerGuard(heartbeat, pid, uid); - if (uid == BuildConfig.MANAGER_INJECTED_UID) - ensureWebViewPermission(); - return this; - } - - public boolean isRunningManager(int pid, int uid) { - return false; - } - - void onSystemServerDied() { - guard = null; - } - - @Override - public IBinder asBinder() { - return this; - } - - @Override - public int getXposedApiVersion() { - return IXposedService.LIB_API; - } - - @Override - public long getXposedVersionCode() { - return BuildConfig.VERSION_CODE; - } - - @Override - public String getXposedVersionName() { - return BuildConfig.VERSION_NAME; - } - - @Override - public String getApi() { - return ConfigManager.getInstance().getApi(); - } - - @Override - public ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) throws RemoteException { - return PackageService.getInstalledPackagesFromAllUsers(flags, filterNoProcess); - } - - @Override - public String[] enabledModules() { - return ConfigManager.getInstance().enabledModules(); - } - - @Override - public boolean enableModule(String packageName) throws RemoteException { - return ConfigManager.getInstance().enableModule(packageName); - } - - @Override - public boolean setModuleScope(String packageName, List scope) throws RemoteException { - return ConfigManager.getInstance().setModuleScope(packageName, scope); - } - - @Override - public List getModuleScope(String packageName) { - return ConfigManager.getInstance().getModuleScope(packageName); - } - - @Override - public boolean disableModule(String packageName) { - return ConfigManager.getInstance().disableModule(packageName); - } - - @Override - public boolean isVerboseLog() { - return ConfigManager.getInstance().verboseLog(); - } - - @Override - public void setVerboseLog(boolean enabled) { - ConfigManager.getInstance().setVerboseLog(enabled); - } - - @Override - public ParcelFileDescriptor getVerboseLog() { - return ConfigManager.getInstance().getVerboseLog(); - } - - @Override - public ParcelFileDescriptor getModulesLog() { - ServiceManager.getLogcatService().checkLogFile(); - return ConfigManager.getInstance().getModulesLog(); - } - - @Override - public boolean clearLogs(boolean verbose) { - return ConfigManager.getInstance().clearLogs(verbose); - } - - @Override - public PackageInfo getPackageInfo(String packageName, int flags, int uid) throws RemoteException { - return PackageService.getPackageInfo(packageName, flags, uid); - } - - @Override - public void forceStopPackage(String packageName, int userId) throws RemoteException { - ActivityManagerService.forceStopPackage(packageName, userId); - } - - @Override - public void reboot() throws RemoteException { - PowerService.reboot(false, null, false); - } - - @Override - public boolean uninstallPackage(String packageName, int userId) throws RemoteException { - try { - if (ActivityManagerService.startUserInBackground(userId)) { - var pkg = new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST); - return PackageService.uninstallPackage(pkg, userId); - } else { - return false; - } - } catch (InterruptedException | ReflectiveOperationException e) { - Log.e(TAG, e.getMessage(), e); - return false; - } - } - - @Override - public boolean isSepolicyLoaded() { - return SELinux.checkSELinuxAccess("u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", - "file", "execute_no_trans"); - } - - @Override - public List getUsers() throws RemoteException { - var users = new LinkedList(); - for (var user : UserService.getUsers()) { - var info = new UserInfo(); - info.id = user.id; - info.name = user.name; - users.add(info); - } - return users; - } - - @Override - public int installExistingPackageAsUser(String packageName, int userId) { - try { - if (ActivityManagerService.startUserInBackground(userId)) - return PackageService.installExistingPackageAsUser(packageName, userId); - else return PackageService.INSTALL_FAILED_INTERNAL_ERROR; - } catch (Throwable e) { - Log.w(TAG, "install existing package as user: ", e); - return PackageService.INSTALL_FAILED_INTERNAL_ERROR; - } - } - - @Override - public boolean systemServerRequested() { - return ServiceManager.systemServerRequested(); - } - - @Override - public int startActivityAsUserWithFeature(Intent intent, int userId) throws RemoteException { - if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { - intent.removeExtra("lsp_no_switch_to_user"); - var currentUser = ActivityManagerService.getCurrentUser(); - if (currentUser == null) return -1; - var parent = UserService.getProfileParent(userId); - if (parent < 0) return -1; - if (currentUser.id != parent) { - if (!ActivityManagerService.switchUser(parent)) return -1; - var window = android.os.ServiceManager.getService(Context.WINDOW_SERVICE); - if (window != null) { - var wm = IWindowManager.Stub.asInterface(window); - wm.lockNow(null); - } - } - } - return ActivityManagerService.startActivityAsUserWithFeature("android", null, intent, intent.getType(), null, null, 0, 0, null, null, userId); - } - - @Override - public ParcelableListSlice queryIntentActivitiesAsUser(Intent intent, int flags, int userId) throws RemoteException { - return PackageService.queryIntentActivities(intent, intent.getType(), flags, userId); - } - - @Override - public boolean dex2oatFlagsLoaded() { - return SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0"); - } - - @Override - public void setHiddenIcon(boolean hide) { - Bundle args = new Bundle(); - args.putString("value", hide ? "0" : "1"); - args.putString("_user", "0"); - try { - var contentProvider = ActivityManagerService.getContentProvider("settings", 0); - if (contentProvider != null) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - contentProvider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), - "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - contentProvider.call("android", null, "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - contentProvider.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } - } catch (NoSuchMethodError e) { - Log.w(TAG, "setHiddenIcon: ", e); - } - } - } catch (Throwable e) { - Log.w(TAG, "setHiddenIcon: ", e); - } - } - - @Override - public void getLogs(ParcelFileDescriptor zipFd) { - ConfigFileManager.getLogs(zipFd); - } - - @Override - public void restartFor(Intent intent) throws RemoteException { - } - - @Override - public List getDenyListPackages() { - return ConfigManager.getInstance().getDenyListPackages(); - } - - @Override - public void flashZip(String zipPath, ParcelFileDescriptor outputStream) { - var processBuilder = new ProcessBuilder("magisk", "--install-module", zipPath); - var fd = new File("/proc/self/fd/" + outputStream.getFd()); - processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(fd)); - try (outputStream; var fdw = new FileOutputStream(fd, true)) { - var proc = processBuilder.start(); - if (proc.waitFor(10, TimeUnit.SECONDS)) { - var exit = proc.exitValue(); - if (exit == 0) { - fdw.write("- Reboot after 5s\n".getBytes()); - Thread.sleep(5000); - reboot(); - } else { - var s = "! Flash failed, exit with " + exit + "\n"; - fdw.write(s.getBytes()); - } - } else { - proc.destroy(); - fdw.write("! Timeout, abort\n".getBytes()); - } - } catch (IOException | InterruptedException | RemoteException e) { - Log.e(TAG, "flashZip: ", e); - } - } - - @Override - public void clearApplicationProfileData(String packageName) throws RemoteException { - PackageService.clearApplicationProfileData(packageName); - } - - @Override - public boolean enableStatusNotification() { - return ConfigManager.getInstance().enableStatusNotification(); - } - - @Override - public void setEnableStatusNotification(boolean enable) { - ConfigManager.getInstance().setEnableStatusNotification(enable); - if (enable) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - @Override - public boolean performDexOptMode(String packageName) throws RemoteException { - return PackageService.performDexOptMode(packageName); - } - - @Override - public boolean getDexObfuscate() { - return ConfigManager.getInstance().dexObfuscate(); - } - - @Override - public void setDexObfuscate(boolean enabled) { - ConfigManager.getInstance().setDexObfuscate(enabled); - } - - @Override - public int getDex2OatWrapperCompatibility() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return ServiceManager.getDex2OatService().getCompatibility(); - } else { - return 0; - } - } - - @Override - public void setLogWatchdog(boolean enabled) { - ConfigManager.getInstance().setLogWatchdog(enabled); - } - - @Override - public boolean isLogWatchdogEnabled() { - return ConfigManager.getInstance().isLogWatchdogEnabled(); - } - - @Override - public boolean setAutoInclude(String packageName, boolean enabled) { - return ConfigManager.getInstance().setAutoInclude(packageName, enabled); - } - - @Override - public boolean getAutoInclude(String packageName) { - return ConfigManager.getInstance().getAutoInclude(packageName); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java deleted file mode 100644 index 1b770c3cb..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; - -import android.content.AttributionSource; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.util.ArrayMap; -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.Module; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedScopeCallback; -import io.github.libxposed.service.IXposedService; - -public class LSPModuleService extends IXposedService.Stub { - - private final static String TAG = "LSPosedModuleService"; - - private final static Set uidSet = ConcurrentHashMap.newKeySet(); - private final static Map serviceMap = Collections.synchronizedMap(new WeakHashMap<>()); - - public final static String FILES_DIR = "files"; - - private final @NonNull - Module loadedModule; - - static void uidClear() { - uidSet.clear(); - } - - static void uidStarts(int uid) { - if (!uidSet.contains(uid)) { - uidSet.add(uid); - var module = ConfigManager.getInstance().getModule(uid); - if (module != null && module.file != null && !module.file.legacy) { - var service = serviceMap.computeIfAbsent(module, LSPModuleService::new); - service.sendBinder(uid); - } - } - } - - static void uidGone(int uid) { - uidSet.remove(uid); - } - - private void sendBinder(int uid) { - var name = loadedModule.packageName; - try { - int userId = uid / PackageService.PER_USER_RANGE; - var authority = name + AUTHORITY_SUFFIX; - var provider = ActivityManagerService.getContentProvider(authority, userId); - if (provider == null) { - Log.d(TAG, "no service provider for " + name); - return; - } - var extra = new Bundle(); - extra.putBinder("binder", asBinder()); - Bundle reply = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - reply = provider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), authority, SEND_BINDER, null, extra); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - reply = provider.call("android", null, authority, SEND_BINDER, null, extra); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - reply = provider.call("android", authority, SEND_BINDER, null, extra); - } else { - reply = provider.call("android", SEND_BINDER, null, extra); - } - if (reply != null) { - Log.d(TAG, "sent module binder to " + name); - } else { - Log.w(TAG, "failed to send module binder to " + name); - } - } catch (Throwable e) { - Log.w(TAG, "failed to send module binder for uid " + uid, e); - } - } - - LSPModuleService(@NonNull Module module) { - loadedModule = module; - } - - private int ensureModule() throws RemoteException { - var appId = Binder.getCallingUid() % PER_USER_RANGE; - if (loadedModule.appId != appId) { - throw new RemoteException("Module " + loadedModule.packageName + " is not for uid " + Binder.getCallingUid()); - } - return Binder.getCallingUid() / PER_USER_RANGE; - } - - @Override - public int getApiVersion() throws RemoteException { - ensureModule(); - return IXposedService.LIB_API; - } - - @Override - public String getFrameworkName() throws RemoteException { - ensureModule(); - return BuildConfig.FRAMEWORK_NAME; - } - - @Override - public String getFrameworkVersion() throws RemoteException { - ensureModule(); - return BuildConfig.VERSION_NAME; - } - - @Override - public long getFrameworkVersionCode() throws RemoteException { - ensureModule(); - return BuildConfig.VERSION_CODE; - } - - @Override - public long getFrameworkProperties() throws RemoteException { - ensureModule(); - var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; - if (ConfigManager.getInstance().dexObfuscate()) { - prop = prop | IXposedService.PROP_RT_API_PROTECTION; - } - return prop; - } - - @Override - public List getScope() throws RemoteException { - ensureModule(); - ArrayList res = new ArrayList<>(); - var scope = ConfigManager.getInstance().getModuleScope(loadedModule.packageName); - if (scope == null) return res; - for (var s : scope) { - res.add(s.packageName); - } - return res; - } - - @Override - public void requestScope(List packages, IXposedScopeCallback callback) throws RemoteException { - var userId = ensureModule(); - if (!ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { - for (String packageName : packages) { - LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); - } - } else { - callback.onScopeRequestFailed("Scope request blocked by user configuration"); - } - } - - @Override - public void removeScope(List packages) throws RemoteException { - var userId = ensureModule(); - for (String packageName : packages) { - try { - if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { - Log.w(TAG, "Failed to remove scope: " + packageName + " (Invalid request)"); - } - } catch (Throwable e) { - Log.e(TAG, "Error removing scope for " + packageName, e); - } - } - } - - @Override - public Bundle requestRemotePreferences(String group) throws RemoteException { - var userId = ensureModule(); - var bundle = new Bundle(); - bundle.putSerializable("map", ConfigManager.getInstance().getModulePrefs(loadedModule.packageName, userId, group)); - return bundle; - } - - @Override - public void updateRemotePreferences(String group, Bundle diff) throws RemoteException { - var userId = ensureModule(); - Map values = new ArrayMap<>(); - if (diff.containsKey("delete")) { - var deletes = (Set) diff.getSerializable("delete"); - for (var key : deletes) { - values.put((String) key, null); - } - } - if (diff.containsKey("put")) { - try { - var puts = (Map) diff.getSerializable("put"); - for (var entry : puts.entrySet()) { - values.put((String) entry.getKey(), entry.getValue()); - } - } catch (Throwable e) { - Log.e(TAG, "updateRemotePreferences: ", e); - } - } - try { - ConfigManager.getInstance().updateModulePrefs(loadedModule.packageName, userId, group, values); - ((LSPInjectedModuleService) loadedModule.service).onUpdateRemotePreferences(group, diff); - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public void deleteRemotePreferences(String group) throws RemoteException { - var userId = ensureModule(); - ConfigManager.getInstance().deleteModulePrefs(loadedModule.packageName, userId, group); - } - - @Override - public String[] listRemoteFiles() throws RemoteException { - var userId = ensureModule(); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - var files = dir.toFile().list(); - return files == null ? new String[0] : files; - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public ParcelFileDescriptor openRemoteFile(String path) throws RemoteException { - var userId = ensureModule(); - ConfigFileManager.ensureModuleFilePath(path); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - return ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public boolean deleteRemoteFile(String path) throws RemoteException { - var userId = ensureModule(); - ConfigFileManager.ensureModuleFilePath(path); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - return dir.resolve(path).toFile().delete(); - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java deleted file mode 100644 index e2ee5148e..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ /dev/null @@ -1,332 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.INotificationManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ParceledListSlice; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.daemon.R; -import org.lsposed.lspd.util.FakeContext; - -import java.util.ArrayList; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedScopeCallback; - -public class LSPNotificationManager { - static final String UPDATED_CHANNEL_ID = "lsposed_module_updated"; - static final String SCOPE_CHANNEL_ID = "lsposed_module_scope"; - private static final String STATUS_CHANNEL_ID = "lsposed_status"; - private static final int STATUS_NOTIFICATION_ID = 2000; - private static final String opPkg = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? - "android" : "com.android.settings"; - - private static final Map notificationIds = new ConcurrentHashMap<>(); - private static int previousNotificationId = STATUS_NOTIFICATION_ID; - - static final String openManagerAction = UUID.randomUUID().toString(); - static final String moduleScope = UUID.randomUUID().toString(); - - private static INotificationManager notificationManager = null; - private static IBinder binder = null; - - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "notificationManager is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - notificationManager = null; - } - }; - - private static INotificationManager getNotificationManager() throws RemoteException { - if (binder == null || notificationManager == null) { - binder = android.os.ServiceManager.getService(Context.NOTIFICATION_SERVICE); - binder.linkToDeath(recipient, 0); - notificationManager = INotificationManager.Stub.asInterface(binder); - } - return notificationManager; - } - - private static Bitmap getBitmap(int id) { - var r = ConfigFileManager.getResources(); - var res = r.getDrawable(id, r.newTheme()); - if (res instanceof BitmapDrawable) { - return ((BitmapDrawable) res).getBitmap(); - } else { - if (res instanceof AdaptiveIconDrawable) { - var layers = new Drawable[]{((AdaptiveIconDrawable) res).getBackground(), - ((AdaptiveIconDrawable) res).getForeground()}; - res = new LayerDrawable(layers); - } - var bitmap = Bitmap.createBitmap(res.getIntrinsicWidth(), - res.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - var canvas = new Canvas(bitmap); - res.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - res.draw(canvas); - return bitmap; - } - } - - private static Icon getNotificationIcon() { - return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)); - } - - private static boolean hasNotificationChannelForSystem( - INotificationManager nm, String channelId) throws RemoteException { - NotificationChannel channel; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - channel = nm.getNotificationChannelForPackage("android", 1000, channelId, null, false); - } else { - channel = nm.getNotificationChannelForPackage("android", 1000, channelId, false); - } - if (channel != null) { - Log.d(TAG, "hasNotificationChannelForSystem: " + channel); - } - return channel != null; - } - - private static void createNotificationChannel(INotificationManager nm) throws RemoteException { - var context = new FakeContext(); - var list = new ArrayList(); - - var updated = new NotificationChannel(UPDATED_CHANNEL_ID, - context.getString(R.string.module_updated_channel_name), - NotificationManager.IMPORTANCE_HIGH); - updated.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, UPDATED_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + UPDATED_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, updated); - } else { - list.add(updated); - } - - var status = new NotificationChannel(STATUS_CHANNEL_ID, - context.getString(R.string.status_channel_name), - NotificationManager.IMPORTANCE_MIN); - status.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, STATUS_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + STATUS_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, status); - } else { - list.add(status); - } - - var scope = new NotificationChannel(SCOPE_CHANNEL_ID, - context.getString(R.string.scope_channel_name), - NotificationManager.IMPORTANCE_HIGH); - scope.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, SCOPE_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + SCOPE_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, scope); - } else { - list.add(scope); - } - - Log.d(TAG, "create notification channels for android: " + list); - nm.createNotificationChannelsForPackage("android", 1000, new ParceledListSlice<>(list)); - } - - static void notifyStatusNotification() { - var intent = new Intent(openManagerAction); - intent.setPackage("android"); - var context = new FakeContext(); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - var notification = new Notification.Builder(context, STATUS_CHANNEL_ID) - .setContentTitle(context.getString(R.string.lsposed_running_notification_title)) - .setContentText(context.getString(R.string.lsposed_running_notification_content)) - .setSmallIcon(getNotificationIcon()) - .setContentIntent(PendingIntent.getBroadcast(context, 1, intent, flags)) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setOngoing(true) - .setAutoCancel(false) - .build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - createNotificationChannel(nm); - nm.enqueueNotificationWithTag("android", opPkg, null, - STATUS_NOTIFICATION_ID, notification, 0); - } catch (RemoteException e) { - Log.e(TAG, "notifyStatusNotification: ", e); - } - } - - static void cancelStatusNotification() { - try { - var nm = getNotificationManager(); - createNotificationChannel(nm); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm.cancelNotificationWithTag("android", "android", null, STATUS_NOTIFICATION_ID, 0); - } else { - nm.cancelNotificationWithTag("android", null, STATUS_NOTIFICATION_ID, 0); - } - } catch (RemoteException e) { - Log.e(TAG, "cancelStatusNotification: ", e); - } - } - - private static PendingIntent getModuleIntent(String modulePackageName, int moduleUserId) { - var intent = new Intent(openManagerAction); - intent.setPackage("android"); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).build()); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast(new FakeContext(), 3, intent, flags); - } - - private static PendingIntent getModuleScopeIntent(String modulePackageName, int moduleUserId, String scopePackageName, String action, IXposedScopeCallback callback) { - var intent = new Intent(moduleScope); - intent.setPackage("android"); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).encodedPath(scopePackageName).appendQueryParameter("action", action).build()); - var extras = new Bundle(); - extras.putBinder("callback", callback.asBinder()); - intent.putExtras(extras); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast(new FakeContext(), 4, intent, flags); - } - - private static String getNotificationIdKey(String channel, String modulePackageName, int moduleUserId) { - return channel + "/" + modulePackageName + ":" + moduleUserId; - } - - private static int pushAndGetNotificationId(String channel, String modulePackageName, int moduleUserId) { - var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId); - // previousNotificationId start with 2001 - // https://android.googlesource.com/platform/frameworks/base/+/master/proto/src/system_messages.proto - // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h - // (AID_APP_END - AID_APP_START) x10 = 100000 < NOTE_NETWORK_AVAILABLE - return notificationIds.computeIfAbsent(idKey, key -> previousNotificationId++); - } - - static void notifyModuleUpdated(String modulePackageName, - int moduleUserId, - boolean enabled, - boolean systemModule) { - var context = new FakeContext(); - var userName = UserService.getUserName(moduleUserId); - String title = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_title_system : - R.string.xposed_module_updated_notification_title : - R.string.module_is_not_activated_yet); - String content = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_content_system : - R.string.xposed_module_updated_notification_content : - (moduleUserId == 0 ? - R.string.module_is_not_activated_yet_main_user_detailed : - R.string.module_is_not_activated_yet_multi_user_detailed), modulePackageName, userName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, UPDATED_CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setContentIntent(getModuleIntent(modulePackageName, moduleUserId)) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setAutoCancel(true) - .setStyle(style) - .build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, - pushAndGetNotificationId(UPDATED_CHANNEL_ID, modulePackageName, moduleUserId), - notification, 0); - } catch (RemoteException e) { - Log.e(TAG, "notify module updated", e); - } - } - - static void requestModuleScope(String modulePackageName, int moduleUserId, String scopePackageName, IXposedScopeCallback callback) { - var context = new FakeContext(); - var userName = UserService.getUserName(moduleUserId); - String title = context.getString(R.string.xposed_module_request_scope_title); - String content = context.getString(R.string.xposed_module_request_scope_content, modulePackageName, userName, scopePackageName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, SCOPE_CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setAutoCancel(true) - .setTimeoutAfter(1000 * 60 * 60) - .setStyle(style) - .setDeleteIntent(getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "delete", callback)) - .setActions(new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_check_24), - context.getString(R.string.scope_approve), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "approve", callback)) - .build(), - new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_close_24), - context.getString(R.string.scope_deny), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "deny", callback)) - .build(), - new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_block_24), - context.getString(R.string.nerver_ask_again), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback)) - .build() - ).build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, - pushAndGetNotificationId(SCOPE_CHANNEL_ID, modulePackageName, moduleUserId), - notification, 0); - } catch (RemoteException e) { - try { - callback.onScopeRequestFailed(e.getMessage()); - } catch (RemoteException ignored) { - } - Log.e(TAG, "request module scope", e); - } - } - - static void cancelNotification(String channel, String modulePackageName, int moduleUserId) { - try { - var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId); - var idValue = notificationIds.get(idKey); - if (idValue == null) return; - var nm = getNotificationManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm.cancelNotificationWithTag("android", "android", modulePackageName, idValue, 0); - } else { - nm.cancelNotificationWithTag("android", modulePackageName, idValue, 0); - } - notificationIds.remove(idKey); - } catch (RemoteException e) { - Log.e(TAG, "cancel notification", e); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java deleted file mode 100644 index facf75417..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.getSystemServiceManager; - -import android.os.Build; -import android.os.IBinder; -import android.os.IServiceCallback; -import android.os.Parcel; -import android.os.RemoteException; -import android.os.SystemProperties; -import android.util.Log; - -public class LSPSystemServerService extends ILSPSystemServerService.Stub implements IBinder.DeathRecipient { - - private final String proxyServiceName; - private IBinder originService = null; - private int requested; - - public boolean systemServerRequested() { - return requested > 0; - } - - public void putBinderForSystemServer() { - android.os.ServiceManager.addService(proxyServiceName, this); - binderDied(); - } - - public LSPSystemServerService(int maxRetry, String serviceName) { - Log.d(TAG, "LSPSystemServerService::LSPSystemServerService with proxy " + serviceName); - proxyServiceName = serviceName; - requested = -maxRetry; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Registers a callback when system is registering an authentic "serial" service - // And we are proxying all requests to that system service - var serviceCallback = new IServiceCallback.Stub() { - @Override - public void onRegistration(String name, IBinder binder) { - Log.d(TAG, "LSPSystemServerService::LSPSystemServerService onRegistration: " + name + " " + binder); - if (name.equals(proxyServiceName) && binder != null && binder != LSPSystemServerService.this) { - Log.d(TAG, "Register " + name + " " + binder); - originService = binder; - LSPSystemServerService.this.linkToDeath(); - } - } - - @Override - public IBinder asBinder() { - return this; - } - }; - try { - getSystemServiceManager().registerForNotifications(proxyServiceName, serviceCallback); - } catch (Throwable e) { - Log.e(TAG, "unregister: ", e); - } - } - } - - @Override - public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - Log.d(TAG, "ILSPApplicationService.requestApplicationService: " + uid + " " + pid + " " + processName + " " + heartBeat); - requested = 1; - if (ConfigManager.getInstance().shouldSkipSystemServer() || uid != 1000 || heartBeat == null || !"system".equals(processName)) - return null; - else - return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat); - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - Log.d(TAG, "LSPSystemServerService.onTransact: code=" + code); - if (originService != null) { - return originService.transact(code, data, reply, flags); - } - - switch (code) { - case BridgeService.TRANSACTION_CODE -> { - int uid = data.readInt(); - int pid = data.readInt(); - String processName = data.readString(); - IBinder heartBeat = data.readStrongBinder(); - var service = requestApplicationService(uid, pid, processName, heartBeat); - if (service != null) { - Log.d(TAG, "LSPSystemServerService.onTransact requestApplicationService granted: " + service); - reply.writeNoException(); - reply.writeStrongBinder(service.asBinder()); - return true; - } else { - Log.d(TAG, "LSPSystemServerService.onTransact requestApplicationService rejected"); - return false; - } - } - case LSPApplicationService.OBFUSCATION_MAP_TRANSACTION_CODE, LSPApplicationService.DEX_TRANSACTION_CODE -> { - // Proxy LSP dex transaction to Application Binder - return ServiceManager.getApplicationService().onTransact(code, data, reply, flags); - } - default -> { - return super.onTransact(code, data, reply, flags); - } - } - } - - public void linkToDeath() { - try { - originService.linkToDeath(this, 0); - } catch (Throwable e) { - Log.e(TAG, "system server service: link to death", e); - } - } - - @Override - public void binderDied() { - if (originService != null) { - originService.unlinkToDeath(this, 0); - originService = null; - } - } - - public void maybeRetryInject() { - if (requested < 0) { - Log.w(TAG, "System server injection fails, trying a restart"); - ++requested; - if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && Build.SUPPORTED_32_BIT_ABIS.length > 0) { - // Only devices with both 32-bit and 64-bit support have zygote_secondary - SystemProperties.set("ctl.restart", "zygote_secondary"); - } else { - SystemProperties.set("ctl.restart", "zygote"); - } - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java deleted file mode 100644 index 4af2632a4..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.Intent.EXTRA_UID; -import static org.lsposed.lspd.service.LSPNotificationManager.SCOPE_CHANNEL_ID; -import static org.lsposed.lspd.service.LSPNotificationManager.UPDATED_CHANNEL_ID; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.getExecutorService; - -import android.app.IApplicationThread; -import android.app.IUidObserver; -import android.content.Context; -import android.content.IIntentReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.provider.Telephony; -import android.telephony.TelephonyManager; -import android.util.Log; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.Application; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.zip.ZipFile; - -import hidden.HiddenApiBridge; -import io.github.libxposed.service.IXposedScopeCallback; - -public class LSPosedService extends ILSPosedService.Stub { - private static final int AID_NOBODY = 9999; - private static final int USER_NULL = -10000; - private static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED"; - public static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED"; - private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle"; - private static final String EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS"; - private static boolean bootCompleted = false; - private IBinder appThread = null; - - private static boolean isModernModules(ApplicationInfo info) { - String[] apks; - if (info.splitSourceDirs != null) { - apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); - apks[info.splitSourceDirs.length] = info.sourceDir; - } else apks = new String[]{info.sourceDir}; - for (var apk : apks) { - try (var zip = new ZipFile(apk)) { - if (zip.getEntry("META-INF/xposed/java_init.list") != null) { - return true; - } - } catch (IOException ignored) { - } - } - return false; - } - - @Override - public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - if (Binder.getCallingUid() != 1000) { - Log.w(TAG, "Someone else got my binder!?"); - return null; - } - if (ServiceManager.getApplicationService().hasRegister(uid, pid)) { - Log.d(TAG, "Skipped duplicated request for uid " + uid + " pid " + pid); - return null; - } - if (!ServiceManager.getManagerService().shouldStartManager(pid, uid, processName) && ConfigManager.getInstance().shouldSkipProcess(new ConfigManager.ProcessScope(processName, uid))) { - Log.d(TAG, "Skipped " + processName + "/" + uid); - return null; - } - Log.d(TAG, "returned service"); - return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat); - } - - /** - * This part is quite complex. - * For modules, we never care about its user id, we only care about its apk path. - * So we will only process module's removal when it's removed from all users. - * And FULLY_REMOVE is exactly the one. - *

- * For applications, we care about its user id. - * So we will process application's removal when it's removed from every single user. - * However, PACKAGE_REMOVED will be triggered by `pm hide`, so we use UID_REMOVED instead. - */ - - private void dispatchPackageChanged(Intent intent) { - if (intent == null) return; - int uid = intent.getIntExtra(EXTRA_UID, AID_NOBODY); - if (uid == AID_NOBODY || uid <= 0) return; - int userId = intent.getIntExtra("android.intent.extra.user_handle", USER_NULL); - var intentAction = intent.getAction(); - if (intentAction == null) return; - var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); - if (userId == USER_NULL) userId = uid % PER_USER_RANGE; - Uri uri = intent.getData(); - var module = ConfigManager.getInstance().getModule(uid); - String moduleName = (uri != null) ? uri.getSchemeSpecificPart() : (module != null) ? module.packageName : null; - - ApplicationInfo applicationInfo = null; - if (moduleName != null) { - try { - applicationInfo = PackageService.getApplicationInfo(moduleName, PackageManager.GET_META_DATA | PackageService.MATCH_ALL_FLAGS, 0); - } catch (Throwable ignored) { - } - } - - boolean isXposedModule = applicationInfo != null && ((applicationInfo.metaData != null && applicationInfo.metaData.containsKey("xposedminversion")) || isModernModules(applicationInfo)); - - switch (intentAction) { - case Intent.ACTION_PACKAGE_FULLY_REMOVED -> { - // for module, remove module - // because we only care about when the apk is gone - if (moduleName != null) { - if (allUsers && ConfigManager.getInstance().removeModule(moduleName)) { - isXposedModule = true; - broadcastAndShowNotification(moduleName, userId, intent, true); - } - LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId); - } - } - case Intent.ACTION_PACKAGE_REMOVED -> { - if (moduleName != null) { - LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId); - } - break; - } - case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED -> { - var configManager = ConfigManager.getInstance(); - // make sure that the change is for the complete package, not only a - // component - String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); - if (components != null && !Arrays.stream(components).reduce(false, (p, c) -> p || c.equals(moduleName), Boolean::logicalOr)) { - return; - } - if (isXposedModule) { - // When installing a new Xposed module, we update the apk path to mark it as a - // module to send a broadcast when modules that have not been activated are - // uninstalled. - // If cache not updated, assume it's not xposed module - isXposedModule = configManager.updateModuleApkPath(moduleName, ConfigManager.getInstance().getModuleApkPath(applicationInfo), false); - } else { - if (configManager.isUidHooked(uid)) { - // it will automatically remove obsolete app from database - configManager.updateAppCache(); - } - if (intentAction.equals(Intent.ACTION_PACKAGE_ADDED) && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { - for (String xposedModule : configManager.getAutoIncludeModules()) { - // For Xposed modules with auto_include set, we always add new applications - // to its scope - var list = configManager.getModuleScope(xposedModule); - if (list != null) { - Application scope = new Application(); - scope.packageName = moduleName; - scope.userId = userId; - list.add(scope); - try { - if (!configManager.setModuleScope(xposedModule, list)) { - Log.e(TAG, "failed to set scope for " + xposedModule); - } - } catch(RemoteException re) { - Log.e(TAG, "failed to set scope for " + xposedModule, re); - } - } - } - } - } - broadcastAndShowNotification(moduleName, userId, intent, isXposedModule); - } - case Intent.ACTION_UID_REMOVED -> { - // when a package is removed (rather than hide) for a single user - // (apk may still be there because of multi-user) - broadcastAndShowNotification(moduleName, userId, intent, isXposedModule); - if (isXposedModule) { - // it will auto remove obsolete app and scope from database - ConfigManager.getInstance().updateCache(); - } else if (ConfigManager.getInstance().isUidHooked(uid)) { - // it will auto remove obsolete scope from database - ConfigManager.getInstance().updateAppCache(); - } - } - } - boolean removed = Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intentAction) || Intent.ACTION_UID_REMOVED.equals(intentAction); - - Log.d(TAG, "Package changed: uid=" + uid + " userId=" + userId + " action=" + intentAction + " isXposedModule=" + isXposedModule + " isAllUsers=" + allUsers); - - if (BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(moduleName) && userId == 0) { - Log.d(TAG, "Manager updated"); - ConfigManager.getInstance().updateManager(removed); - } - } - - private void broadcastAndShowNotification(String packageName, int userId, Intent intent, boolean isXposedModule) { - Log.d(TAG, "package " + packageName + " changed, dispatching to manager"); - var action = intent.getAction(); - var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); - intent.putExtra("android.intent.extra.PACKAGES", packageName); - intent.putExtra(Intent.EXTRA_USER, userId); - intent.putExtra("isXposedModule", isXposedModule); - LSPManagerService.broadcastIntent(intent); - if (isXposedModule) { - var enabledModules = ConfigManager.getInstance().enabledModules(); - var scope = ConfigManager.getInstance().getModuleScope(packageName); - boolean systemModule = scope != null && scope.parallelStream().anyMatch(app -> app.packageName.equals("system")); - boolean enabled = Arrays.asList(enabledModules).contains(packageName); - if (!(Intent.ACTION_UID_REMOVED.equals(action) || Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action) || allUsers)) - LSPNotificationManager.notifyModuleUpdated(packageName, userId, enabled, systemModule); - } - } - - private void dispatchUserChanged(Intent intent) { - if (intent == null) return; - int uid = intent.getIntExtra(EXTRA_USER_HANDLE, AID_NOBODY); - if (uid == AID_NOBODY || uid <= 0) return; - LSPManagerService.broadcastIntent(intent); - } - - private void dispatchBootCompleted(Intent intent) { - bootCompleted = true; - var configManager = ConfigManager.getInstance(); - if (configManager.enableStatusNotification()) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - private void dispatchConfigurationChanged(Intent intent) { - if (!bootCompleted) return; - ConfigFileManager.reloadConfiguration(); - var configManager = ConfigManager.getInstance(); - if (configManager.enableStatusNotification()) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - private void dispatchSecretCodeReceive(Intent i) { - LSPManagerService.openManager(null); - } - - private void dispatchOpenManager(Intent intent) { - LSPManagerService.openManager(intent.getData()); - } - - private void dispatchModuleScope(Intent intent) { - Log.d(TAG, "dispatchModuleScope: " + intent); - var data = intent.getData(); - var extras = intent.getExtras(); - if (extras == null || data == null) return; - var callback = extras.getBinder("callback"); - if (callback == null || !callback.isBinderAlive()) return; - var authority = data.getEncodedAuthority(); - if (authority == null) return; - var s = authority.split(":", 2); - if (s.length != 2) return; - var packageName = s[0]; - int userId; - try { - userId = Integer.parseInt(s[1]); - } catch (NumberFormatException e) { - return; - } - var scopePackageName = data.getPath(); - if (scopePackageName == null) return; - scopePackageName = scopePackageName.substring(1); - var action = data.getQueryParameter("action"); - if (action == null) return; - - var iCallback = IXposedScopeCallback.Stub.asInterface(callback); - try { - var applicationInfo = PackageService.getApplicationInfo(scopePackageName, 0, userId); - if (applicationInfo == null) { - iCallback.onScopeRequestFailed("Package not found"); - return; - } - - switch (action) { - case "approve" -> { - ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId); - iCallback.onScopeRequestApproved(Collections.singletonList(scopePackageName)); - } - case "deny" -> iCallback.onScopeRequestFailed("Request denied by user"); - case "delete" -> iCallback.onScopeRequestFailed("Request timeout"); - case "block" -> { - ConfigManager.getInstance().blockScopeRequest(packageName); - iCallback.onScopeRequestFailed("Request blocked by configuration"); - } - } - Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId); - } catch (RemoteException e) { - try { - iCallback.onScopeRequestFailed(e.getMessage()); - } catch (RemoteException ignored) { - // callback died - } - } - LSPNotificationManager.cancelNotification(SCOPE_CHANNEL_ID, packageName, userId); - } - - private void registerReceiver(List filters, String requiredPermission, int userId, Consumer task, int flag) { - var receiver = new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> { - try { - task.accept(intent); - } catch (Throwable t) { - Log.e(TAG, "performReceive: ", t); - } - }); - if (!ordered && !Objects.equals(intent.getAction(), Intent.ACTION_LOCKED_BOOT_COMPLETED)) - return; - try { - ActivityManagerService.finishReceiver(this, appThread, resultCode, data, extras, false, intent.getFlags()); - } catch (RemoteException e) { - Log.e(TAG, "finish receiver", e); - } - } - }; - try { - for (var filter : filters) { - ActivityManagerService.registerReceiver("android", null, receiver, filter, requiredPermission, userId, flag); - } - } catch (RemoteException e) { - Log.e(TAG, "register receiver", e); - } - } - - private void registerReceiver(List filters, int userId, Consumer task) { - //noinspection InlinedApi - registerReceiver(filters, "android.permission.BRICK", userId, task, Context.RECEIVER_NOT_EXPORTED); - } - - private void registerPackageReceiver() { - var packageFilter = new IntentFilter(); - packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); - packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); - packageFilter.addDataScheme("package"); - - var uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED); - - registerReceiver(List.of(packageFilter, uidFilter), -1, this::dispatchPackageChanged); - Log.d(TAG, "registered package receiver"); - } - - private void registerConfigurationReceiver() { - var intentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - registerReceiver(List.of(intentFilter), 0, this::dispatchConfigurationChanged); - Log.d(TAG, "registered configuration receiver"); - } - - private void registerSecretCodeReceiver() { - IntentFilter intentFilter = new IntentFilter(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - intentFilter.addAction(TelephonyManager.ACTION_SECRET_CODE); - } else { - // noinspection InlinedApi - intentFilter.addAction(Telephony.Sms.Intents.SECRET_CODE_ACTION); - } - intentFilter.addDataAuthority("5776733", null); - intentFilter.addDataScheme("android_secret_code"); - - //noinspection InlinedApi - registerReceiver(List.of(intentFilter), "android.permission.CONTROL_INCALL_EXPERIENCE", - 0, this::dispatchSecretCodeReceive, Context.RECEIVER_EXPORTED); - Log.d(TAG, "registered secret code receiver"); - } - - private void registerBootCompleteReceiver() { - var intentFilter = new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED); - intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - registerReceiver(List.of(intentFilter), 0, this::dispatchBootCompleted); - Log.d(TAG, "registered boot receiver"); - } - - private void registerUserChangeReceiver() { - var userFilter = new IntentFilter(); - userFilter.addAction(ACTION_USER_ADDED); - userFilter.addAction(ACTION_USER_REMOVED); - - registerReceiver(List.of(userFilter), -1, this::dispatchUserChanged); - Log.d(TAG, "registered user info change receiver"); - } - - private void registerOpenManagerReceiver() { - var intentFilter = new IntentFilter(LSPNotificationManager.openManagerAction); - var moduleFilter = new IntentFilter(intentFilter); - moduleFilter.addDataScheme("module"); - - registerReceiver(List.of(intentFilter, moduleFilter), 0, this::dispatchOpenManager); - Log.d(TAG, "registered open manager receiver"); - } - - private void registerModuleScopeReceiver() { - var intentFilter = new IntentFilter(LSPNotificationManager.moduleScope); - intentFilter.addDataScheme("module"); - - registerReceiver(List.of(intentFilter), 0, this::dispatchModuleScope); - Log.d(TAG, "registered module scope receiver"); - } - - private void registerUidObserver() { - try { - var which = HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED(); - LSPModuleService.uidClear(); - ActivityManagerService.registerUidObserver(new IUidObserver.Stub() { - @Override - public void onUidActive(int uid) { - LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidCachedChanged(int uid, boolean cached) { - if (!cached) LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidIdle(int uid, boolean disabled) { - LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidGone(int uid, boolean disabled) { - LSPModuleService.uidGone(uid); - } - }, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN(), null); - } catch (RemoteException e) { - Log.e(TAG, "registerUidObserver", e); - } - } - - @Override - public void dispatchSystemServerContext(IBinder appThread, IBinder activityToken, String api) { - Log.d(TAG, "received system context"); - this.appThread = appThread; - ConfigManager.getInstance().setApi(api); - ActivityManagerService.onSystemServerContext(IApplicationThread.Stub.asInterface(appThread), activityToken); - registerBootCompleteReceiver(); - registerPackageReceiver(); - registerConfigurationReceiver(); - registerSecretCodeReceiver(); - registerUserChangeReceiver(); - registerOpenManagerReceiver(); - registerModuleScopeReceiver(); - registerUidObserver(); - - if (ServiceManager.isLateInject) { - Log.i(TAG, "System already booted during late injection. Manually triggering boot completed."); - dispatchBootCompleted(null); - } - } - - @Override - public boolean preStartManager() { - return ServiceManager.getManagerService().preStartManager(); - } - - @Override - public boolean setManagerEnabled(boolean enabled) throws RemoteException { - return ServiceManager.getManagerService().setEnabled(enabled); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java b/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java deleted file mode 100644 index 74ba4fae9..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java +++ /dev/null @@ -1,222 +0,0 @@ -package org.lsposed.lspd.service; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.SELinux; -import android.os.SystemProperties; -import android.system.Os; -import android.util.Log; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedHashMap; - -public class LogcatService implements Runnable { - private static final String TAG = "LSPosedLogcat"; - private static final int mode = ParcelFileDescriptor.MODE_WRITE_ONLY | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE | - ParcelFileDescriptor.MODE_APPEND; - private int modulesFd = -1; - private int verboseFd = -1; - private Thread thread = null; - - static class LogLRU extends LinkedHashMap { - private static final int MAX_ENTRIES = 10; - - public LogLRU() { - super(MAX_ENTRIES, 1f, false); - } - - @Override - synchronized protected boolean removeEldestEntry(Entry eldest) { - if (size() > MAX_ENTRIES && eldest.getKey().delete()) { - Log.d(TAG, "Deleted old log " + eldest.getKey().getAbsolutePath()); - return true; - } - return false; - } - } - - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final LinkedHashMap moduleLogs = new LogLRU(); - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final LinkedHashMap verboseLogs = new LogLRU(); - - @SuppressLint("UnsafeDynamicallyLoadedCode") - public LogcatService() { - String classPath = System.getProperty("java.class.path"); - var abi = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]; - System.load(classPath + "!/lib/" + abi + "/" + System.mapLibraryName("daemon")); - ConfigFileManager.moveLogDir(); - - // Meizu devices set this prop and prevent debug logs from being recorded - if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) { - SystemProperties.set("persist.sys.log_reject_level", "0"); - } - - getprop(); - dmesg(); - } - - private static void getprop() { - // multithreaded process can not change their context type, - // start a new process to set restricted context to filter privacy props - var cmd = "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop"; - try { - SELinux.setFSCreateContext("u:object_r:app_data_file:s0"); - new ProcessBuilder("sh", "-c", cmd) - .redirectOutput(ConfigFileManager.getPropsPath()) - .start(); - } catch (IOException e) { - Log.e(TAG, "getprop: ", e); - } finally { - SELinux.setFSCreateContext(null); - } - } - - private static void dmesg() { - try { - new ProcessBuilder("dmesg") - .redirectOutput(ConfigFileManager.getKmsgPath()) - .start(); - } catch (IOException e) { - Log.e(TAG, "dmesg: ", e); - } - } - - private native void runLogcat(); - - @Override - public void run() { - Log.i(TAG, "start running"); - runLogcat(); - Log.i(TAG, "stopped"); - } - - @SuppressWarnings("unused") - private int refreshFd(boolean isVerboseLog) { - try { - File log; - if (isVerboseLog) { - checkFd(verboseFd); - log = ConfigFileManager.getNewVerboseLogPath(); - } else { - checkFd(modulesFd); - log = ConfigFileManager.getNewModulesLogPath(); - } - Log.i(TAG, "New log file: " + log); - ConfigFileManager.chattr0(log.toPath().getParent()); - int fd = ParcelFileDescriptor.open(log, mode).detachFd(); - if (isVerboseLog) { - synchronized (verboseLogs) { - verboseLogs.put(log, new Object()); - } - verboseFd = fd; - } else { - synchronized (moduleLogs) { - moduleLogs.put(log, new Object()); - } - modulesFd = fd; - } - return fd; - } catch (IOException e) { - if (isVerboseLog) verboseFd = -1; - else modulesFd = -1; - Log.w(TAG, "refreshFd", e); - return -1; - } - } - - private static void checkFd(int fd) { - if (fd == -1) return; - try { - var jfd = new FileDescriptor(); - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - jfd.getClass().getDeclaredMethod("setInt$", int.class).invoke(jfd, fd); - var stat = Os.fstat(jfd); - if (stat.st_nlink == 0) { - var file = Files.readSymbolicLink(fdToPath(fd)); - var parent = file.getParent(); - if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { - if (ConfigFileManager.chattr0(parent)) - Files.deleteIfExists(parent); - } - var name = file.getFileName().toString(); - var originName = name.substring(0, name.lastIndexOf(' ')); - Files.copy(file, parent.resolve(originName)); - } - } catch (Throwable e) { - Log.w(TAG, "checkFd " + fd, e); - } - } - - public boolean isRunning() { - return thread != null && thread.isAlive(); - } - - public void start() { - if (isRunning()) return; - thread = new Thread(this); - thread.setName("logcat"); - thread.setUncaughtExceptionHandler((t, e) -> { - Log.e(TAG, "Crash unexpectedly: ", e); - thread = null; - start(); - }); - thread.start(); - } - - public void startVerbose() { - Log.i(TAG, "!!start_verbose!!"); - } - - public void stopVerbose() { - Log.i(TAG, "!!stop_verbose!!"); - } - - public void enableWatchdog() { - Log.i(TAG, "!!start_watchdog!!"); - } - - public void disableWatchdog() { - Log.i(TAG, "!!stop_watchdog!!"); - } - - public void refresh(boolean isVerboseLog) { - if (isVerboseLog) { - Log.i(TAG, "!!refresh_verbose!!"); - } else { - Log.i(TAG, "!!refresh_modules!!"); - } - } - - private static Path fdToPath(int fd) { - if (fd == -1) return null; - else return Paths.get("/proc/self/fd", String.valueOf(fd)); - } - - public File getVerboseLog() { - var path = fdToPath(verboseFd); - return path == null ? null : path.toFile(); - } - - public File getModulesLog() { - var path = fdToPath(modulesFd); - return path == null ? null : path.toFile(); - } - - public void checkLogFile() { - if (modulesFd == -1) - refresh(false); - if (verboseFd == -1) - refresh(true); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java deleted file mode 100644 index 4446f2366..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.lsposed.lspd.service; - -import android.os.SharedMemory; - -import java.util.HashMap; - -public class ObfuscationManager { - // For module dexes - static native SharedMemory obfuscateDex(SharedMemory memory); - - // generates signature - static native HashMap getSignatures(); -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java b/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java deleted file mode 100644 index 8c7a07a1d..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java +++ /dev/null @@ -1,431 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace; - -import android.content.IIntentReceiver; -import android.content.IIntentSender; -import android.content.Intent; -import android.content.IntentSender; -import android.content.pm.ApplicationInfo; -import android.content.pm.ComponentInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.pm.VersionedPackage; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemProperties; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.models.Application; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; - -import rikka.parcelablelist.ParcelableListSlice; - -public class PackageService { - - static final int INSTALL_FAILED_INTERNAL_ERROR = -110; - static final int INSTALL_REASON_UNKNOWN = 0; - static final int MATCH_ANY_USER = 0x00400000; // PackageManager.MATCH_ANY_USER - - static final int MATCH_ALL_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER; - public static final int PER_USER_RANGE = 100000; - - private static IPackageManager pm = null; - private static IBinder binder = null; - private static final Method getInstalledPackagesMethod; - - static { - Method method = null; - try { - boolean isLongFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; - Class flagsType = isLongFlags ? long.class : int.class; - - for (Method m : IPackageManager.class.getDeclaredMethods()) { - if (m.getName().equals("getInstalledPackages") && - m.getParameterTypes().length == 2 && - m.getParameterTypes()[0] == flagsType) { - m.setAccessible(true); - method = m; - break; - } - } - } catch (Exception e) { - Log.e("PackageManagerUtils", "Failed to find getInstalledPackages method", e); - } - getInstalledPackagesMethod = method; - } - - private static List getInstalledPackagesReflect(IPackageManager pm, Object flags, int userId) { - if (getInstalledPackagesMethod == null || pm == null) - return Collections.emptyList(); - try { - Object result = getInstalledPackagesMethod.invoke(pm, flags, userId); - if (result instanceof ParceledListSlice) { - // noinspection unchecked - return ((ParceledListSlice) result).getList(); - } - } catch (Exception e) { - Log.w("PackageManagerUtils", "Reflection call failed", e); - } - return Collections.emptyList(); - } - - static boolean isAlive() { - var pm = getPackageManager(); - return pm != null && pm.asBinder().isBinderAlive(); - } - - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "pm is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - pm = null; - } - }; - - private static IPackageManager getPackageManager() { - if (binder == null || pm == null) { - binder = ServiceManager.getService("package"); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - pm = IPackageManager.Stub.asInterface(binder); - } - return pm; - } - - @Nullable - public static PackageInfo getPackageInfo(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getPackageInfo(packageName, (long) flags, userId); - } - return pm.getPackageInfo(packageName, flags, userId); - } - - public static @NonNull - Map getPackageInfoFromAllUsers(String packageName, int flags) throws RemoteException { - IPackageManager pm = getPackageManager(); - Map res = new HashMap<>(); - if (pm == null) return res; - for (var user : UserService.getUsers()) { - var info = getPackageInfo(packageName, flags, user.id); - if (info != null && info.applicationInfo != null) res.put(user.id, info); - } - return res; - } - - @Nullable - public static ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getApplicationInfo(packageName, (long) flags, userId); - } - return pm.getApplicationInfo(packageName, flags, userId); - } - - // Only for manager - public static ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) - throws RemoteException { - List res = new ArrayList<>(); - IPackageManager pm = getPackageManager(); - if (pm == null) - return ParcelableListSlice.emptyList(); - // Prepare flags once outside the loop - Object flagsObj = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) ? (long) flags : flags; - for (var user : UserService.getUsers()) { - // Use the reflective helper instead of direct AIDL calls - List infos = getInstalledPackagesReflect(pm, flagsObj, user.id); - res.addAll(infos.parallelStream() - .filter(info -> info.applicationInfo != null - && info.applicationInfo.uid / PER_USER_RANGE == user.id) - .filter(info -> { - try { - return isPackageAvailable(info.packageName, user.id, true); - } catch (RemoteException e) { - return false; - } - }) - .collect(Collectors.toList())); - } - if (filterNoProcess) { - return new ParcelableListSlice<>(res.parallelStream().filter(packageInfo -> { - try { - PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / PER_USER_RANGE); - return !fetchProcesses(pkgInfo).isEmpty(); - } catch (RemoteException e) { - Log.w(TAG, "filter failed", e); - return true; - } - }).collect(Collectors.toList())); - } - return new ParcelableListSlice<>(res); - } - - private static Set fetchProcesses(PackageInfo pkgInfo) { - HashSet processNames = new HashSet<>(); - if (pkgInfo == null) return processNames; - for (ComponentInfo[] componentInfos : new ComponentInfo[][]{pkgInfo.activities, pkgInfo.receivers, pkgInfo.providers}) { - if (componentInfos == null) continue; - for (ComponentInfo componentInfo : componentInfos) { - processNames.add(componentInfo.processName); - } - } - if (pkgInfo.services == null) return processNames; - for (ServiceInfo service : pkgInfo.services) { - if ((service.flags & FLAG_ISOLATED_PROCESS) == 0) { - processNames.add(service.processName); - } - } - return processNames; - } - - public static Pair, Integer> fetchProcessesWithUid(Application app) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return new Pair<>(Collections.emptySet(), -1); - PackageInfo pkgInfo = getPackageInfoWithComponents(app.packageName, MATCH_ALL_FLAGS, app.userId); - if (pkgInfo == null || pkgInfo.applicationInfo == null) - return new Pair<>(Collections.emptySet(), -1); - return new Pair<>(fetchProcesses(pkgInfo), pkgInfo.applicationInfo.uid); - } - - public static boolean isPackageAvailable(String packageName, int userId, boolean ignoreHidden) throws RemoteException { - return pm.isPackageAvailable(packageName, userId) || (ignoreHidden && pm.getApplicationHiddenSettingAsUser(packageName, userId)); - } - - @SuppressWarnings({"ConstantConditions", "SameParameterValue"}) - @Nullable - private static PackageInfo getPackageInfoWithComponents(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - PackageInfo pkgInfo; - try { - pkgInfo = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS, userId); - } catch (Exception e) { - pkgInfo = getPackageInfo(packageName, flags, userId); - if (pkgInfo == null) return null; - try { - pkgInfo.activities = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES, userId).activities; - } catch (Exception ignored) { - - } - try { - pkgInfo.services = getPackageInfo(packageName, flags | PackageManager.GET_SERVICES, userId).services; - } catch (Exception ignored) { - - } - try { - pkgInfo.receivers = getPackageInfo(packageName, flags | PackageManager.GET_RECEIVERS, userId).receivers; - } catch (Exception ignored) { - - } - try { - pkgInfo.providers = getPackageInfo(packageName, flags | PackageManager.GET_PROVIDERS, userId).providers; - } catch (Exception ignored) { - - } - } - if (pkgInfo == null || pkgInfo.applicationInfo == null || (!pkgInfo.packageName.equals("android") && (pkgInfo.applicationInfo.sourceDir == null || !existsInGlobalNamespace(pkgInfo.applicationInfo.sourceDir) || !isPackageAvailable(packageName, userId, true)))) - return null; - return pkgInfo; - } - - static abstract class IntentSenderAdaptor extends IIntentSender.Stub { - public abstract void send(Intent intent); - - @Override - public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - send(intent); - return 0; - } - - @Override - public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - send(intent); - } - - public IntentSender getIntentSender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { - @SuppressWarnings("JavaReflectionMemberAccess") - Constructor intentSenderConstructor = IntentSender.class.getConstructor(IIntentSender.class); - intentSenderConstructor.setAccessible(true); - return intentSenderConstructor.newInstance(this); - } - } - - public static boolean uninstallPackage(VersionedPackage versionedPackage, int userId) throws RemoteException, InterruptedException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { - CountDownLatch latch = new CountDownLatch(1); - final boolean[] result = {false}; - var flag = userId == -1 ? 0x00000002 : 0; //PackageManager.DELETE_ALL_USERS = 0x00000002; UserHandle ALL = new UserHandle(-1); - pm.getPackageInstaller().uninstall(versionedPackage, "android", flag, new IntentSenderAdaptor() { - @Override - public void send(Intent intent) { - int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); - result[0] = status == PackageInstaller.STATUS_SUCCESS; - Log.d(TAG, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)); - latch.countDown(); - } - }.getIntentSender(), userId == -1 ? 0 : userId); - latch.await(); - return result[0]; - } - - public static int installExistingPackageAsUser(String packageName, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - Log.d(TAG, "about to install existing package " + packageName + "/" + userId); - if (pm == null) return INSTALL_FAILED_INTERNAL_ERROR; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN, null); - } else { - return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN); - } - } - - @Nullable - public static ParcelableListSlice queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) { - try { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - ParceledListSlice infos; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - infos = pm.queryIntentActivities(intent, resolvedType, (long) flags, userId); - } else { - infos = pm.queryIntentActivities(intent, resolvedType, flags, userId); - } - return new ParcelableListSlice<>(infos.getList()); - } catch (Exception e) { - Log.e(TAG, "queryIntentActivities", e); - return new ParcelableListSlice<>(new ArrayList<>()); - } - } - - @Nullable - public static Intent getLaunchIntentForPackage(String packageName) throws RemoteException { - Intent intentToResolve = new Intent(Intent.ACTION_MAIN); - intentToResolve.addCategory(Intent.CATEGORY_INFO); - intentToResolve.setPackage(packageName); - var ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0); - - // Otherwise, try to find a main launcher activity. - if (ris == null || ris.getList().size() == 0) { - // reuse the intent instance - intentToResolve.removeCategory(Intent.CATEGORY_INFO); - intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); - intentToResolve.setPackage(packageName); - ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0); - } - if (ris == null || ris.getList().size() == 0) { - return null; - } - Intent intent = new Intent(intentToResolve); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName(ris.getList().get(0).activityInfo.packageName, - ris.getList().get(0).activityInfo.name); - return intent; - } - - public static void clearApplicationProfileData(String packageName) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return; - pm.clearApplicationProfileData(packageName); - } - - public static boolean performDexOptMode(String packageName) throws RemoteException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Process process = null; - try { - // The 'speed-profile' filter is a balanced choice for performance. - String command = "cmd package compile -m speed-profile -f " + packageName; - process = Runtime.getRuntime().exec(command); - - // Capture and log the output for debugging. - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - } - - int exitCode = process.waitFor(); - Log.i(TAG, "Dexopt command finished for " + packageName + " with exit code: " + exitCode); - - // A successful command returns exit code 0 and typically "Success" in its output. - return exitCode == 0 && output.toString().contains("Success"); - - } catch (Exception e) { - Log.e(TAG, "Failed to execute dexopt shell command for " + packageName, e); - if (e instanceof InterruptedException) { - // Preserve the interrupted status. - Thread.currentThread().interrupt(); - } - return false; - } finally { - if (process != null) { - process.destroy(); - } - } - } else { - // Fallback to the original reflection method for older Android versions. - IPackageManager pm = getPackageManager(); - if (pm == null) return false; - return pm.performDexOptMode(packageName, - SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), - SystemProperties.get("pm.dexopt.install", "speed-profile"), true, true, null); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java b/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java deleted file mode 100644 index 7ce1ac5b6..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * - */ - -package org.lsposed.lspd.service; - -import static android.content.Context.POWER_SERVICE; -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.os.IBinder; -import android.os.IPowerManager; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class PowerService { - private static IPowerManager pm = null; - private static IBinder binder = null; - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "PowerManager is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - pm = null; - } - }; - - private static IPowerManager getPowerManager() { - if (binder == null || pm == null) { - binder = ServiceManager.getService(POWER_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - pm = IPowerManager.Stub.asInterface(binder); - } - return pm; - } - - public static void reboot(boolean confirm, String reason, boolean wait) throws RemoteException { - IPowerManager pm = getPowerManager(); - if (pm == null) return; - pm.reboot(confirm, reason, wait); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java deleted file mode 100644 index e0fbc16af..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import android.app.ActivityThread; -import android.app.Notification; -import android.content.Context; -import android.ddm.DdmHandleAppName; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.os.IServiceManager; -import android.os.Looper; -import android.os.Parcel; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import com.android.internal.os.BinderInternal; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.util.FakeContext; - -import java.io.File; -import java.lang.AbstractMethodError; -import java.lang.Class; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import hidden.HiddenApiBridge; - -public class ServiceManager { - public static final String TAG = "LSPosedService"; - private static final File globalNamespace = new File("/proc/1/root"); - @SuppressWarnings("FieldCanBeLocal") - private static LSPosedService mainService = null; - private static LSPApplicationService applicationService = null; - private static LSPManagerService managerService = null; - private static LSPSystemServerService systemServerService = null; - private static LogcatService logcatService = null; - private static Dex2OatService dex2OatService = null; - - public static boolean isLateInject = false; - public static String proxyServiceName = "serial"; - - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - @RequiresApi(Build.VERSION_CODES.Q) - public static Dex2OatService getDex2OatService() { - return dex2OatService; - } - - public static ExecutorService getExecutorService() { - return executorService; - } - - private static void waitSystemService(String name) { - while (android.os.ServiceManager.getService(name) == null) { - try { - Log.i(TAG, "service " + name + " is not started, wait 1s."); - //noinspection BusyWait - Thread.sleep(1000); - } catch (InterruptedException e) { - Log.i(TAG, Log.getStackTraceString(e)); - } - } - } - - public static IServiceManager getSystemServiceManager() { - return IServiceManager.Stub.asInterface(HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject())); - } - - // call by ourselves - public static void start(String[] args) { - if (!ConfigFileManager.tryLock()) System.exit(0); - - int systemServerMaxRetry = 1; - for (String arg : args) { - if (arg.startsWith("--system-server-max-retry=")) { - try { - systemServerMaxRetry = Integer.parseInt(arg.substring(arg.lastIndexOf('=') + 1)); - } catch (Throwable ignored) { - } - } else if (arg.equals("--late-inject")) { - isLateInject = true; - proxyServiceName = "serial_vector"; - } - } - - Log.i(TAG, "Vector daemon started: lateInject: " + isLateInject); - Log.i(TAG, String.format("version %s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - - Thread.setDefaultUncaughtExceptionHandler((t, e) -> { - Log.e(TAG, "Uncaught exception", e); - System.exit(1); - }); - - logcatService = new LogcatService(); - logcatService.start(); - - // get config before package service is started - // otherwise getInstance will trigger module/scope cache - var configManager = ConfigManager.getInstance(); - // --- DO NOT call ConfigManager.getInstance later!!! --- - - // Unblock log watchdog before starting anything else - if (configManager.isLogWatchdogEnabled()) - logcatService.enableWatchdog(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - permissionManagerWorkaround(); - - Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); - Looper.prepareMainLooper(); - - - mainService = new LSPosedService(); - applicationService = new LSPApplicationService(); - managerService = new LSPManagerService(); - systemServerService = new LSPSystemServerService(systemServerMaxRetry, proxyServiceName); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - dex2OatService = new Dex2OatService(); - dex2OatService.start(); - } - - systemServerService.putBinderForSystemServer(); - - ActivityThread.systemMain(); - - DdmHandleAppName.setAppName("org.lsposed.daemon", 0); - - waitSystemService("package"); - waitSystemService("activity"); - waitSystemService(Context.USER_SERVICE); - waitSystemService(Context.APP_OPS_SERVICE); - - ConfigFileManager.reloadConfiguration(); - - notificationWorkaround(); - - BridgeService.send(mainService, new BridgeService.Listener() { - @Override - public void onSystemServerRestarted() { - Log.w(TAG, "system restarted..."); - } - - @Override - public void onResponseFromBridgeService(boolean response) { - if (response) { - Log.i(TAG, "sent service to bridge"); - } else { - Log.w(TAG, "no response from bridge"); - } - systemServerService.maybeRetryInject(); - } - - @Override - public void onSystemServerDied() { - Log.w(TAG, "system server died"); - systemServerService.putBinderForSystemServer(); - managerService.onSystemServerDied(); - } - }); - - // Force logging on boot, now let's see if we need to stop logging - if (!configManager.verboseLog()) { - logcatService.stopVerbose(); - } - - Looper.loop(); - throw new RuntimeException("Main thread loop unexpectedly exited"); - } - - public static LSPApplicationService getApplicationService() { - return applicationService; - } - - public static LSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - if (applicationService.registerHeartBeat(uid, pid, processName, heartBeat)) - return applicationService; - else return null; - } - - public static LSPManagerService getManagerService() { - return managerService; - } - - public static LogcatService getLogcatService() { - return logcatService; - } - - public static boolean systemServerRequested() { - return systemServerService.systemServerRequested(); - } - - public static File toGlobalNamespace(File file) { - return new File(globalNamespace, file.getAbsolutePath()); - } - - public static File toGlobalNamespace(String path) { - if (path == null) return null; - if (path.startsWith("/")) return new File(globalNamespace, path); - else return toGlobalNamespace(new File(path)); - } - - public static boolean existsInGlobalNamespace(File file) { - return toGlobalNamespace(file).exists(); - } - - public static boolean existsInGlobalNamespace(String path) { - return toGlobalNamespace(path).exists(); - } - - private static void permissionManagerWorkaround() { - try { - Field sCacheField = android.os.ServiceManager.class.getDeclaredField("sCache"); - sCacheField.setAccessible(true); - var sCache = (Map) sCacheField.get(null); - sCache.put("permissionmgr", new BinderProxy("permissionmgr")); - sCache.put("legacy_permission", new BinderProxy("legacy_permission")); - sCache.put("appops", new BinderProxy("appops")); - } catch (Throwable e) { - Log.e(TAG, "failed to init permission manager", e); - } - } - - private static void notificationWorkaround() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - try { - Class feature = Class.forName("android.app.FeatureFlagsImpl"); - Field systemui_is_cached = feature.getDeclaredField("systemui_is_cached"); - systemui_is_cached.setAccessible(true); - systemui_is_cached.set(null, true); - Log.d(TAG, "set flag systemui_is_cached to true"); - } catch (Throwable e) { - Log.e(TAG, "failed to change feature flags", e); - } - } - - try { - new Notification.Builder(new FakeContext(), "notification_workaround").build(); - } catch (AbstractMethodError e) { - FakeContext.nullProvider = ! FakeContext.nullProvider; - } catch (Throwable e) { - Log.e(TAG, "failed to build notifications", e); - } - - } - - private static class BinderProxy extends Binder { - private static final Method rawGetService; - - static { - try { - rawGetService = android.os.ServiceManager.class.getDeclaredMethod("rawGetService", String.class); - rawGetService.setAccessible(true); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - private IBinder mReal = null; - private final String mName; - - BinderProxy(String name) { - mName = name; - } - - @Override - protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { - synchronized (this) { - if (mReal == null) { - try { - mReal = (IBinder) rawGetService.invoke(null, mName); - } catch (IllegalAccessException | InvocationTargetException ignored){ - - } - } - if (mReal != null) { - return mReal.transact(code, data, reply, flags); - } - } - // getSplitPermissions - if (reply != null && mName.equals("permissionmgr")) - reply.writeTypedList(List.of()); - return true; - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/UserService.java b/daemon/src/main/java/org/lsposed/lspd/service/UserService.java deleted file mode 100644 index f6eee5597..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/UserService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.content.Context; -import android.content.pm.UserInfo; -import android.os.Build; -import android.os.IBinder; -import android.os.IUserManager; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -import org.lsposed.lspd.util.Utils; - -import java.util.LinkedList; -import java.util.List; - -public class UserService { - private static IUserManager um = null; - private static IBinder binder = null; - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "um is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - um = null; - } - }; - - static boolean isAlive() { - var um = getUserManager(); - return um != null && um.asBinder().isBinderAlive(); - } - - public static IUserManager getUserManager() { - if (binder == null || um == null) { - binder = ServiceManager.getService(Context.USER_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - um = IUserManager.Stub.asInterface(binder); - } - return um; - } - - public static List getUsers() throws RemoteException { - IUserManager um = getUserManager(); - List users = new LinkedList<>(); - if (um == null) return users; - try { - users = um.getUsers(true); - } catch (NoSuchMethodError e) { - users = um.getUsers(true, true, true); - } - if (Utils.isLENOVO) { // lenovo hides user [900, 910) for app cloning - var gotUsers = new boolean[10]; - for (var user : users) { - var residual = user.id - 900; - if (residual >= 0 && residual < 10) gotUsers[residual] = true; - } - for (int i = 900; i <= 909; i++) { - var user = um.getUserInfo(i); - if (user != null && !gotUsers[i - 900]) { - users.add(user); - } - } - } - return users; - } - - public static UserInfo getUserInfo(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return null; - return um.getUserInfo(userId); - } - - public static String getUserName(int userId) { - try { - var userInfo = getUserInfo(userId); - if (userInfo != null) return userInfo.name; - } catch (RemoteException ignored) { - } - return String.valueOf(userId); - } - - public static int getProfileParent(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return -1; - var userInfo = um.getProfileParent(userId); - if (userInfo == null) return userId; - else return userInfo.id; - } - - public static boolean isUserUnlocked(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return false; - return um.isUserUnlocked(userId); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java b/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java deleted file mode 100644 index ce5d5a3ad..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.os.UserHandle; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.lsposed.lspd.service.ConfigFileManager; -import org.lsposed.lspd.service.PackageService; - -import hidden.HiddenApiBridge; - -public class FakeContext extends ContextWrapper { - static ApplicationInfo systemApplicationInfo = null; - static Resources.Theme theme = null; - - public static Boolean nullProvider = false; - - private String packageName = "android"; - public FakeContext() { - super(null); - } - - public FakeContext(String packageName) { - super(null); - this.packageName = packageName; - } - - @Override - public String getPackageName() { - return packageName; - } - - @Override - public Resources getResources() { - return ConfigFileManager.getResources(); - } - - @Override - public String getOpPackageName() { - return "android"; - } - - @Override - public ApplicationInfo getApplicationInfo() { - try { - if (systemApplicationInfo == null) - systemApplicationInfo = PackageService.getApplicationInfo("android", 0, 0); - } catch (Throwable e) { - Log.e(TAG, "getApplicationInfo", e); - } - return systemApplicationInfo; - } - - @Override - public ContentResolver getContentResolver() { - if (nullProvider) { - return null; - } else { - return new ContentResolver(this) {}; - } - } - - public int getUserId() { - return 0; - } - - public UserHandle getUser() { - return HiddenApiBridge.UserHandle(0); - } - - @Override - public Resources.Theme getTheme() { - if (theme == null) theme = getResources().newTheme(); - return theme; - } - - @Nullable - @Override - public String getAttributionTag() { - return null; - } - - @Override - public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException { - throw new PackageManager.NameNotFoundException(packageName); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java b/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java deleted file mode 100644 index 1d6b81d15..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.lsposed.lspd.util.SignInfo.CERTIFICATE; - -import com.android.apksig.ApkVerifier; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; - -public class InstallerVerifier { - - public static void verifyInstallerSignature(String path) throws IOException { - ApkVerifier verifier = new ApkVerifier.Builder(new File(path)) - .setMinCheckedPlatformVersion(27) - .build(); - try { - ApkVerifier.Result result = verifier.verify(); - if (!result.isVerified()) { - throw new IOException("apk signature not verified"); - } - var mainCert = result.getSignerCertificates().get(0); - if (!Arrays.equals(mainCert.getEncoded(), CERTIFICATE)) { - var dname = mainCert.getSubjectX500Principal().getName(); - throw new IOException("apk signature mismatch: " + dname); - } - } catch (Exception t) { - throw new IOException(t); - } - } -} diff --git a/daemon/src/main/jni/dex2oat.cpp b/daemon/src/main/jni/dex2oat.cpp index 062e41343..32cf77d07 100644 --- a/daemon/src/main/jni/dex2oat.cpp +++ b/daemon/src/main/jni/dex2oat.cpp @@ -1,21 +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) 2023 LSPosed Contributors - */ #include #include #include @@ -28,50 +10,71 @@ #include "logging.h" -extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_doMountNative( +// Lightweight RAII wrapper to prevent FD leaks +struct UniqueFd { + int fd; + explicit UniqueFd(int fd) : fd(fd) {} + ~UniqueFd() { + if (fd >= 0) close(fd); + } + operator int() const { return fd; } +}; + +extern "C" JNIEXPORT void JNICALL Java_org_matrix_vector_daemon_env_Dex2OatServer_doMountNative( JNIEnv *env, jobject, jboolean enabled, jstring r32, jstring d32, jstring r64, jstring d64) { char dex2oat32[PATH_MAX], dex2oat64[PATH_MAX]; - realpath("bin/dex2oat32", dex2oat32); - realpath("bin/dex2oat64", dex2oat64); + if (realpath("bin/dex2oat32", dex2oat32) == nullptr) { + PLOGE("resolve realpath for bin/dex2oat32"); + } + if (realpath("bin/dex2oat64", dex2oat64) == nullptr) { + PLOGE("resolve realpath for bin/dex2oat64"); + } - if (pid_t pid = fork(); pid > 0) { // parent + const char *r32p = r32 ? env->GetStringUTFChars(r32, nullptr) : nullptr; + const char *d32p = d32 ? env->GetStringUTFChars(d32, nullptr) : nullptr; + const char *r64p = r64 ? env->GetStringUTFChars(r64, nullptr) : nullptr; + const char *d64p = d64 ? env->GetStringUTFChars(d64, nullptr) : nullptr; + + pid_t pid = fork(); + if (pid > 0) { // Parent process waitpid(pid, nullptr, 0); - } else { // child - int ns = open("/proc/1/ns/mnt", O_RDONLY); - setns(ns, CLONE_NEWNS); - close(ns); - const char *r32p, *d32p, *r64p, *d64p; - if (r32) r32p = env->GetStringUTFChars(r32, nullptr); - if (d32) d32p = env->GetStringUTFChars(d32, nullptr); - if (r64) r64p = env->GetStringUTFChars(r64, nullptr); - if (d64) d64p = env->GetStringUTFChars(d64, nullptr); + // Safely release JNI strings in the parent + if (r32p) env->ReleaseStringUTFChars(r32, r32p); + if (d32p) env->ReleaseStringUTFChars(d32, d32p); + if (r64p) env->ReleaseStringUTFChars(r64, r64p); + if (d64p) env->ReleaseStringUTFChars(d64, d64p); + } else if (pid == 0) { // Child process + UniqueFd ns(open("/proc/1/ns/mnt", O_RDONLY)); + if (ns >= 0) { + setns(ns, CLONE_NEWNS); + } if (enabled) { LOGI("Enable dex2oat wrapper"); - if (r32) { + if (r32p) { mount(dex2oat32, r32p, nullptr, MS_BIND, nullptr); mount(nullptr, r32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (d32) { + if (d32p) { mount(dex2oat32, d32p, nullptr, MS_BIND, nullptr); mount(nullptr, d32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (r64) { + if (r64p) { mount(dex2oat64, r64p, nullptr, MS_BIND, nullptr); mount(nullptr, r64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (d64) { + if (d64p) { mount(dex2oat64, d64p, nullptr, MS_BIND, nullptr); mount(nullptr, d64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } execlp("resetprop", "resetprop", "--delete", "dalvik.vm.dex2oat-flags", nullptr); } else { LOGI("Disable dex2oat wrapper"); - if (r32) umount(r32p); - if (d32) umount(d32p); - if (r64) umount(r64p); - if (d64) umount(d64p); + if (r32p) umount(r32p); + if (d32p) umount(d32p); + if (r64p) umount(r64p); + if (d64p) umount(d64p); execlp("resetprop", "resetprop", "dalvik.vm.dex2oat-flags", "--inline-max-code-units=0", nullptr); } @@ -83,8 +86,9 @@ extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_d static int setsockcreatecon_raw(const char *context) { std::string path = "/proc/self/task/" + std::to_string(gettid()) + "/attr/sockcreate"; - int fd = open(path.c_str(), O_RDWR | O_CLOEXEC); + UniqueFd fd(open(path.c_str(), O_RDWR | O_CLOEXEC)); if (fd < 0) return -1; + int ret; if (context) { do { @@ -95,20 +99,19 @@ static int setsockcreatecon_raw(const char *context) { ret = write(fd, nullptr, 0); // clear } while (ret < 0 && errno == EINTR); } - close(fd); return ret < 0 ? -1 : 0; } extern "C" JNIEXPORT jboolean JNICALL -Java_org_lsposed_lspd_service_Dex2OatService_setSockCreateContext(JNIEnv *env, jclass, - jstring contextStr) { - const char *context = env->GetStringUTFChars(contextStr, nullptr); +Java_org_matrix_vector_daemon_env_Dex2OatServer_setSockCreateContext(JNIEnv *env, jclass, + jstring contextStr) { + const char *context = contextStr ? env->GetStringUTFChars(contextStr, nullptr) : nullptr; int ret = setsockcreatecon_raw(context); - env->ReleaseStringUTFChars(contextStr, context); + if (context) env->ReleaseStringUTFChars(contextStr, context); return ret == 0; } extern "C" JNIEXPORT jstring JNICALL -Java_org_lsposed_lspd_service_Dex2OatService_getSockPath(JNIEnv *env, jobject) { +Java_org_matrix_vector_daemon_env_Dex2OatServer_getSockPath(JNIEnv *env, jobject) { return env->NewStringUTF("5291374ceda0aef7c5d86cd2a4f6a3ac\0"); } diff --git a/daemon/src/main/jni/logcat.cpp b/daemon/src/main/jni/logcat.cpp index 2e5897a39..6041efd30 100644 --- a/daemon/src/main/jni/logcat.cpp +++ b/daemon/src/main/jni/logcat.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -217,23 +218,32 @@ void Logcat::ProcessBuffer(struct log_msg *buf) { if (android_log_processLogBuffer(&buf->entry, &entry) < 0) return; entry.tagLen--; - std::string_view tag(entry.tag, entry.tagLen); bool shortcut = false; - if (tag == "LSPosed-Bridge"sv || tag == "XSharedPreferences"sv || tag == "LSPosedContext") + + if (tag == "VectorLegacyBridge"sv || tag == "XSharedPreferences"sv || tag == "VectorContext"sv) [[unlikely]] { modules_print_count_ += PrintLogLine(entry, modules_file_.get()); shortcut = true; } + + constexpr std::array exact_tags = { + "APatchD"sv, "Dobby"sv, "KernelSU"sv, "LSPlant"sv, + "LSPlt"sv, "Magisk"sv, "SELinux"sv, "TEESimulator"sv}; + constexpr std::array prefix_tags = {"dex2oat"sv, "Vector"sv, "LSPosed"sv, + "zygisk"sv}; + + bool match_exact = + std::any_of(exact_tags.begin(), exact_tags.end(), [&](auto t) { return tag == t; }); + bool match_prefix = std::any_of(prefix_tags.begin(), prefix_tags.end(), + [&](auto t) { return tag.starts_with(t); }); + if (verbose_ && (shortcut || buf->id() == log_id::LOG_ID_CRASH || entry.pid == my_pid_ || - tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) || - tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv || - tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv || - tag == "TEESimulator"sv || tag.starts_with("Vector"sv) || - tag.starts_with("zygisk"sv))) [[unlikely]] { + match_exact || match_prefix)) [[unlikely]] { verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } - if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { + + if (entry.pid == my_pid_ && tag == "VectorLogcat"sv) [[unlikely]] { std::string_view msg(entry.message, entry.messageLen); if (msg == "!!start_verbose!!"sv) { verbose_ = true; @@ -354,14 +364,12 @@ void Logcat::Run() { if (modules_print_count_ >= kMaxLogSize) [[unlikely]] RefreshFd(false); } - OnCrash(errno); } } extern "C" JNIEXPORT void JNICALL -// NOLINTNEXTLINE -Java_org_lsposed_lspd_service_LogcatService_runLogcat(JNIEnv *env, jobject thiz) { +Java_org_matrix_vector_daemon_env_LogcatMonitor_runLogcat(JNIEnv *env, jobject thiz) { jclass clazz = env->GetObjectClass(thiz); jmethodID method = env->GetMethodID(clazz, "refreshFd", "(Z)I"); Logcat logcat(env, thiz, method); diff --git a/daemon/src/main/jni/logcat.h b/daemon/src/main/jni/logcat.h index cc39b58a1..e2560ccc3 100644 --- a/daemon/src/main/jni/logcat.h +++ b/daemon/src/main/jni/logcat.h @@ -1,10 +1,9 @@ #pragma once +#include #include #include -#include - #define NS_PER_SEC 1000000000L #define MS_PER_NSEC 1000000 #define LOGGER_ENTRY_MAX_LEN (5 * 1024) @@ -43,15 +42,13 @@ struct log_msg { struct logger_entry entry; }; #ifdef __cplusplus - log_id_t id() { - return static_cast(entry.lid); - } + log_id_t id() { return static_cast(entry.lid); } #endif }; struct logger; struct logger_list; -long android_logger_get_log_size(struct logger* logger); +long android_logger_get_log_size(struct logger *logger); int android_logger_set_log_size(struct logger *logger, unsigned long size); struct logger_list *android_logger_list_alloc(int mode, unsigned int tail, pid_t pid); void android_logger_list_free(struct logger_list *logger_list); diff --git a/daemon/src/main/jni/logging.h b/daemon/src/main/jni/logging.h index 86a7220ac..ca9e36e06 100644 --- a/daemon/src/main/jni/logging.h +++ b/daemon/src/main/jni/logging.h @@ -25,7 +25,7 @@ #include #ifndef LOG_TAG -#define LOG_TAG "LSPosed" +#define LOG_TAG "VectorDaemon" #endif #ifdef LOG_DISABLED diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 9965f42f1..896b491c9 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -145,7 +145,8 @@ static jobject stringMapToJavaHashMap(JNIEnv *env, const std::map = emptyList(), + val options: Map = emptyMap() +) + +data class CliResponse( + val success: Boolean, + val data: Any? = null, + val error: String? = null, + val isFdAttached: Boolean = false +) + +// --- IPC Client Logic --- +object VectorIPC { + val gson: Gson = + GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) // Handles Any/Object fields + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) // Handles Map values + .setPrettyPrinting() + .create() + + fun transmit(request: CliRequest): CliResponse { + val socket = LocalSocket() + return try { + val cliSocket = FileSystem.socketPath.toString() + val socketFile = java.io.File(cliSocket) + + if (!socketFile.exists()) { + System.err.println("Error: Socket file not found at $cliSocket") + System.err.println("Current UID: ${android.os.Process.myUid()}") + } + socket.connect(LocalSocketAddress(cliSocket, LocalSocketAddress.Namespace.FILESYSTEM)) + + val output = DataOutputStream(socket.outputStream) + val input = DataInputStream(socket.inputStream) + + // Send Security Token + output.writeLong(BuildConfig.CLI_TOKEN_MSB) + output.writeLong(BuildConfig.CLI_TOKEN_LSB) + + // Send Request + output.writeUTF(gson.toJson(request)) + + // Read Response + val responseJson = input.readUTF() + val response = gson.fromJson(responseJson, CliResponse::class.java) + + // Handle Log Streaming + if (response.isFdAttached) { + val hasFd = input.readByte() + if (hasFd.toInt() == 1) { + val fds = socket.getAncillaryFileDescriptors() + if (!fds.isNullOrEmpty()) { + streamLog(fds[0], request.options["follow"] as? Boolean ?: false) + } + } + } + response + } catch (e: Exception) { + CliResponse(success = false, error = "Socket Failure: ${e.message}") + } finally { + socket.close() + } + } + + private fun streamLog(fd: java.io.FileDescriptor, follow: Boolean) { + // Wrap the raw FileDescriptor in a FileInputStream. + // 'use' ensures that fis.close() (and thus the FD) is called + // when the block finishes or if an exception is thrown. + FileInputStream(fd).use { fis -> + val reader = fis.bufferedReader() + + try { + while (true) { + val line = reader.readLine() + if (line != null) { + println(line) + } else { + if (!follow) break // EOF reached, exit + + // In follow mode, wait for new data to be written to the log + Thread.sleep(100) + } + + // Check if thread was interrupted (e.g. by a shutdown hook) + if (Thread.interrupted()) break + } + } catch (e: Exception) { + if (e !is InterruptedException) { + System.err.println("Log streaming error: ${e.message}") + } + } + } // FD is closed here automatically + } +} + +// --- UI Formatter --- +object OutputFormatter { + /** + * Auto-formats the Daemon's output. Prints ASCII tables for lists, Key-Value for maps, or raw + * JSON. + */ + @Suppress("UNCHECKED_CAST") + fun print(response: CliResponse, isJson: Boolean): Int { + if (isJson) { + println(VectorIPC.gson.toJson(response)) + return if (response.success) 0 else 1 + } + + if (!response.success) { + System.err.println("Error: ${response.error}") + return 1 + } + + val data = response.data ?: return 0 + + when (data) { + is List<*> -> { + if (data.isEmpty()) { + println("No records found.") + return 0 + } + // Check if it's a list of objects/maps to draw a table + val first = data[0] + if (first is Map<*, *>) { + printTable(data as List>) + } else { + data.forEach { println(" - $it") } + } + } + is Map<*, *> -> { + data.forEach { (k, v) -> println("$k: $v") } + } + else -> println(data.toString()) + } + return 0 + } + + private fun printTable(rows: List>) { + val headers = rows.first().keys.toList() + val columnWidths = headers.associateWith { it.length }.toMutableMap() + + // Calculate maximum width for each column + for (row in rows) { + for (header in headers) { + val length = row[header]?.toString()?.length ?: 0 + if (length > columnWidths[header]!!) { + columnWidths[header] = length + } + } + } + + // Print Headers + val headerRow = headers.joinToString(" ") { it.padEnd(columnWidths[it]!!) } + println(headerRow.uppercase()) + println("-".repeat(headerRow.length)) + + // Print Data + for (row in rows) { + println( + headers.joinToString(" ") { header -> + (row[header]?.toString() ?: "").padEnd(columnWidths[header]!!) + }) + } + } +} + +// --- CLI Commands (picocli) --- +@Command( + name = "vector-cli", + mixinStandardHelpOptions = true, + version = ["Vector CLI ${BuildConfig.VERSION_NAME}"], + description = ["A fast, scriptable CLI for configuring the Vector Framework daemon."], + subcommands = + [ + StatusCommand::class, + ModulesCommand::class, + ScopeCommand::class, + ConfigCommand::class, + LogCommand::class]) +class Cli : Callable { + + @Option( + names = ["--json"], + description = ["Output structured JSON for scripting"], + scope = ScopeType.INHERIT) + var json: Boolean = false + + override fun call(): Int { + CommandLine(this).usage(System.out) + return 0 + } + + companion object { + @JvmStatic + fun main(args: Array) { + val uid = Process.myUid() + if (uid != 0) { + System.err.println("Permission denied: Vector CLI must run as root.") + exitProcess(1) + } + val mainThread = Thread.currentThread() + Runtime.getRuntime() + .addShutdownHook( + Thread { + mainThread.interrupt() // Signal the loop to stop and close the stream + }) + val exitCode = CommandLine(Cli()).execute(*args) + exitProcess(exitCode) + } + } +} + +@Command(name = "status", description = ["Show framework and system health status"]) +class StatusCommand : Callable { + @ParentCommand lateinit var parent: Cli + + override fun call(): Int { + val req = CliRequest(command = "status") + val res = VectorIPC.transmit(req) + return OutputFormatter.print(res, parent.json) + } +} + +@Command(name = "modules", description = ["Manage Xposed modules"]) +class ModulesCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "ls", description = ["List installed modules"]) + fun ls( + @Option(names = ["-e", "--enabled"], description = ["Show only enabled modules"]) + enabled: Boolean, + @Option(names = ["-d", "--disabled"], description = ["Show only disabled modules"]) + disabled: Boolean + ): Int { + val req = + CliRequest( + command = "modules", + action = "ls", + options = mapOf("enabled" to enabled, "disabled" to disabled)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "enable", description = ["Enable one or more modules (batch processing)"]) + fun enable(@Parameters(paramLabel = "PKG", arity = "1..*") pkgs: List): Int { + val req = CliRequest(command = "modules", action = "enable", targets = pkgs) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "disable", description = ["Disable one or more modules (batch processing)"]) + fun disable(@Parameters(paramLabel = "PKG", arity = "1..*") pkgs: List): Int { + val req = CliRequest(command = "modules", action = "disable", targets = pkgs) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "scope", description = ["Manage granular application injection scopes"]) +class ScopeCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "ls", description = ["List apps in a module's scope"]) + fun ls(@Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String): Int { + val req = CliRequest(command = "scope", action = "ls", targets = listOf(modulePkg)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "add", description = ["Append apps to scope (format: pkg/user_id)"]) + fun add( + @Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String, + @Parameters(index = "1..*") apps: List + ): Int { + val req = CliRequest(command = "scope", action = "add", targets = listOf(modulePkg) + apps) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "set", description = ["Overwrite entire scope (format: pkg/user_id)"]) + fun set( + @Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String, + @Parameters(index = "1..*") apps: List + ): Int { + val req = CliRequest(command = "scope", action = "set", targets = listOf(modulePkg) + apps) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "config", description = ["Manage daemon preferences natively"]) +class ConfigCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "get", description = ["Get a config value"]) + fun get(@Parameters(paramLabel = "KEY") key: String): Int { + val req = CliRequest(command = "config", action = "get", targets = listOf(key)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "set", description = ["Set a config value"]) + fun set( + @Parameters(index = "0", paramLabel = "KEY") key: String, + @Parameters(index = "1", paramLabel = "VALUE") value: String + ): Int { + val req = CliRequest(command = "config", action = "set", targets = listOf(key, value)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "log", description = ["Stream or clear framework logs"]) +class LogCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "cat", description = ["Dump logs and exit"]) + fun cat( + @Option(names = ["-v", "--verbose"], description = ["Read verbose daemon log"]) + verbose: Boolean + ): Int { + val req = + CliRequest( + command = "log", + action = "stream", + options = mapOf("verbose" to verbose, "follow" to false)) + VectorIPC.transmit(req) + return 0 + } + + @Command(name = "tail", description = ["Follow logs in real-time"]) + fun tail( + @Option(names = ["-v", "--verbose"], description = ["Follow verbose daemon log"]) + verbose: Boolean + ): Int { + val req = + CliRequest( + command = "log", + action = "stream", + options = mapOf("verbose" to verbose, "follow" to true)) + VectorIPC.transmit(req) + return 0 + } + + @Command(name = "clear", description = ["Clear log buffers"]) + fun clear(@Option(names = ["-v", "--verbose"]) verbose: Boolean): Int { + val req = CliRequest(command = "log", action = "clear", options = mapOf("verbose" to verbose)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt new file mode 100644 index 000000000..96eedb846 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -0,0 +1,105 @@ +package org.matrix.vector.daemon + +import android.app.ActivityThread +import android.content.Context +import android.ddm.DdmHandleAppName +import android.os.Build +import android.os.Looper +import android.os.Process +import android.os.ServiceManager +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.matrix.vector.daemon.core.SystemServerBridge +import org.matrix.vector.daemon.core.VectorService +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.env.CliSocketServer +import org.matrix.vector.daemon.env.Dex2OatServer +import org.matrix.vector.daemon.env.LogcatMonitor +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.ipc.SystemServerService +import org.matrix.vector.daemon.utils.applyNotificationWorkaround + +private const val TAG = "VectorDaemon" + +object VectorDaemon { + var isLateInject = false + var proxyServiceName = "serial" + + @JvmStatic + fun main(args: Array) { + if (!FileSystem.tryLock()) kotlin.system.exitProcess(0) + + var systemServerMaxRetry = 1 + for (arg in args) { + if (arg.startsWith("--system-server-max-retry=")) { + systemServerMaxRetry = arg.substringAfter('=').toIntOrNull() ?: 1 + } else if (arg == "--late-inject") { + isLateInject = true + proxyServiceName = "serial_vector" + } + } + + Log.i(TAG, "Vector daemon started: lateInject=$isLateInject, proxy=$proxyServiceName") + Log.i(TAG, "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + + Thread.setDefaultUncaughtExceptionHandler { _, e -> + Log.e(TAG, "Uncaught exception in Daemon", e) + kotlin.system.exitProcess(1) + } + + // Start Environmental Daemons + LogcatMonitor.start() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Dex2OatServer.start() + } + CliSocketServer.start() + + if (PreferenceStore.isLogWatchdogEnabled()) LogcatMonitor.enableWatchdog() + // Preload Framework DEX in the background + CoroutineScope(Dispatchers.IO).launch { + FileSystem.getPreloadDex(PreferenceStore.isDexObfuscateEnabled()) + } + + // Setup Main Looper & System Services + Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) + @Suppress("DEPRECATION") Looper.prepareMainLooper() + + val systemServerService = SystemServerService(systemServerMaxRetry, proxyServiceName) + systemServerService.putBinderForSystemServer() + + // Initializes system frameworks inside the daemon process + ActivityThread.systemMain() + DdmHandleAppName.setAppName("org.matrix.vector.daemon", 0) + + // Wait for Android Core Services + waitForSystemService("package") + waitForSystemService("activity") + waitForSystemService(Context.USER_SERVICE) + waitForSystemService(Context.APP_OPS_SERVICE) + + applyNotificationWorkaround() + + // Inject Vector into system_server + SystemServerBridge.sendToBridge( + VectorService.asBinder(), isRestart = false, systemServerService) + + if (!ManagerService.isVerboseLog()) { + LogcatMonitor.stopVerbose() + } + + Looper.loop() + throw RuntimeException("Main thread loop unexpectedly exited") + } + + private fun waitForSystemService(name: String) = runBlocking { + while (ServiceManager.getService(name) == null) { + Log.i(TAG, "Waiting system service: $name for 1s") + delay(1000) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt new file mode 100644 index 000000000..e4b159cad --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt @@ -0,0 +1,79 @@ +package org.matrix.vector.daemon.core + +import android.os.IBinder +import android.os.Parcel +import android.system.Os +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.ipc.SystemServerService + +private const val TAG = "VectorBridge" +private const val TRANSACTION_CODE = + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code + +object SystemServerBridge { + + @Suppress("DEPRECATION") + fun sendToBridge(binder: IBinder, isRestart: Boolean, systemServerService: SystemServerService) { + CoroutineScope(Dispatchers.IO).launch { + runCatching { + Os.seteuid(0) + + var bridgeService: IBinder? + while (true) { + bridgeService = android.os.ServiceManager.getService("activity") + if (bridgeService?.pingBinder() == true) break + Log.i(TAG, "activity service not ready, waiting 1s...") + delay(1000) + } + + if (isRestart) Log.w(TAG, "System Server restarted...") + + // Setup death recipient to handle system_server crashes + val deathRecipient = + object : IBinder.DeathRecipient { + override fun binderDied() { + Log.w(TAG, "System Server died! Clearing caches and re-injecting...") + bridgeService.unlinkToDeath(this, 0) + systemServerService.putBinderForSystemServer() + ManagerService.guard = null // ManagerGuard binderDied + sendToBridge(binder, isRestart = true, systemServerService) + } + } + bridgeService.linkToDeath(deathRecipient, 0) + + // Try sending the Binder payload (up to 3 times) + var success = false + for (i in 0 until 3) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + try { + data.writeInt(1) // ACTION_SEND_BINDER + data.writeStrongBinder(binder) + success = bridgeService.transact(TRANSACTION_CODE, data, reply, 0) == true + reply.readException() + if (success) break + } finally { + data.recycle() + reply.recycle() + } + Log.w(TAG, "No response from bridge, retrying...") + delay(1000) + } + + if (success) Log.i(TAG, "Successfully injected Vector into system_server") + else { + Log.e(TAG, "Failed to inject Vector into system_server") + systemServerService.maybeRetryInject() + } + } + .onFailure { Log.e(TAG, "Error during System Server bridging", it) } + .also { if (!BuildConfig.DEBUG) runCatching { Os.seteuid(1000) } } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt new file mode 100644 index 000000000..3a82e9df4 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -0,0 +1,332 @@ +package org.matrix.vector.daemon.core + +import android.app.IApplicationThread +import android.content.Context +import android.content.IIntentReceiver +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.util.Log +import hidden.HiddenApiBridge +import io.github.libxposed.service.IXposedScopeCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.service.ILSPosedService +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.VectorDaemon +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.ManagerService +import org.matrix.vector.daemon.ipc.ModuleService +import org.matrix.vector.daemon.system.* + +private const val TAG = "VectorService" + +object VectorService : ILSPosedService.Stub() { + + private val ioScope = CoroutineScope(Dispatchers.IO) + private var bootCompleted = false + + override fun dispatchSystemServerContext( + appThread: IBinder?, + activityToken: IBinder?, + ) { + appThread?.let { SystemContext.appThread = IApplicationThread.Stub.asInterface(it) } + SystemContext.token = activityToken + + // Initialize OS Observers using Coroutines for the dispatch blocks + registerReceivers() + + if (VectorDaemon.isLateInject) { + Log.i(TAG, "Late injection detected. Forcing boot completed event.") + dispatchBootCompleted() + } + } + + override fun requestApplicationService( + uid: Int, + pid: Int, + processName: String, + heartBeat: IBinder + ): ILSPApplicationService? { + if (Binder.getCallingUid() != 1000) { + Log.w(TAG, "Unauthorized requestApplicationService call") + return null + } + if (ApplicationService.hasRegister(uid, pid)) return null + + val scope = ProcessScope(processName, uid) + if (!ManagerService.shouldStartManager(pid, uid, processName) && + ConfigCache.shouldSkipProcess(scope)) { + Log.d(TAG, "Skipped $processName/$uid") + return null + } + + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { + ApplicationService + } else null + } + + override fun preStartManager() = ManagerService.preStartManager() + + override fun setManagerEnabled(enabled: Boolean) = true // Omitted specific toggle logic + + private fun createReceiver() = + object : IIntentReceiver.Stub() { + override fun performReceive( + intent: Intent, + resultCode: Int, + data: String?, + extras: Bundle?, + ordered: Boolean, + sticky: Boolean, + sendingUser: Int + ) { + ioScope.launch { + when (intent.action) { + Intent.ACTION_LOCKED_BOOT_COMPLETED -> dispatchBootCompleted() + NotificationManager.openManagerAction -> ManagerService.openManager(intent.data) + NotificationManager.moduleScopeAction -> dispatchModuleScope(intent) + else -> dispatchPackageChanged(intent) + } + } + + // Critical for ordered broadcasts to avoid freezing the system queue + if (!ordered && intent.action != Intent.ACTION_LOCKED_BOOT_COMPLETED) return + runCatching { + val appThread = SystemContext.appThread + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.finishReceiver( + appThread?.asBinder(), resultCode, data, extras, false, intent.flags) + } else { + activityManager?.finishReceiver( + this, resultCode, data, extras, false, intent.flags) + } + } + .onFailure { Log.e(TAG, "finishReceiver failed", it) } + } + } + + private fun registerReceivers() { + val packageFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_CHANGED) + addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) + addDataScheme("package") + } + + val uidFilter = IntentFilter(Intent.ACTION_UID_REMOVED) + + val bootFilter = + IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED).apply { + priority = IntentFilter.SYSTEM_HIGH_PRIORITY + } + + val openManagerNoDataFilter = IntentFilter(NotificationManager.openManagerAction) + + val openManagerDataFilter = + IntentFilter(NotificationManager.openManagerAction).apply { + addDataScheme("module") + addDataScheme("android_secret_code") + } + + val scopeFilter = + IntentFilter(NotificationManager.moduleScopeAction).apply { addDataScheme("module") } + val secretCodeFilter = + IntentFilter("android.provider.Telephony.SECRET_CODE").apply { + addDataScheme("android_secret_code") + addDataAuthority("5776733", null) + } + + // Define strict Android 14+ flags and the system-only BRICK permission + val notExported = Context.RECEIVER_NOT_EXPORTED + val exported = Context.RECEIVER_EXPORTED + val brickPerm = "android.permission.BRICK" + + activityManager?.registerReceiverCompat( + createReceiver(), packageFilter, brickPerm, -1, notExported) + activityManager?.registerReceiverCompat(createReceiver(), uidFilter, brickPerm, -1, notExported) + activityManager?.registerReceiverCompat(createReceiver(), bootFilter, brickPerm, 0, notExported) + + activityManager?.registerReceiverCompat( + createReceiver(), openManagerNoDataFilter, brickPerm, 0, notExported) + activityManager?.registerReceiverCompat( + createReceiver(), openManagerDataFilter, brickPerm, 0, notExported) + activityManager?.registerReceiverCompat( + createReceiver(), scopeFilter, brickPerm, 0, notExported) + + // Only the secret dialer code needs to be exported so the phone app can trigger it + activityManager?.registerReceiverCompat( + createReceiver(), + secretCodeFilter, + "android.permission.CONTROL_INCALL_EXPERIENCE", + 0, + exported) + + // UID Observer + val uidObserver = + object : android.app.IUidObserver.Stub() { + override fun onUidActive(uid: Int) = ModuleService.uidStarts(uid) + + override fun onUidCachedChanged(uid: Int, cached: Boolean) { + if (!cached) ModuleService.uidStarts(uid) + } + + override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleService.uidStarts(uid) + + override fun onUidGone(uid: Int, disabled: Boolean) = ModuleService.uidGone(uid) + } + + val which = + HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED() + + activityManager?.registerUidObserverCompat( + uidObserver, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN()) + Log.d(TAG, "Registered all OS Receivers and UID Observers") + } + + private fun dispatchBootCompleted() { + bootCompleted = true + if (PreferenceStore.isStatusNotificationEnabled()) { + NotificationManager.notifyStatusNotification() + } + } + + private fun dispatchPackageChanged(intent: Intent) { + val uid = intent.getIntExtra(Intent.EXTRA_UID, -1) + val action = intent.action ?: return + val userId = intent.getIntExtra("android.intent.extra.user_handle", uid % PER_USER_RANGE) + val uri = intent.data + val moduleName = uri?.schemeSpecificPart ?: ConfigCache.getModuleByUid(uid)?.packageName + + var isXposedModule = false + if (moduleName != null) { + val appInfo = + packageManager + ?.getPackageInfoCompat(moduleName, MATCH_ALL_FLAGS or PackageManager.GET_META_DATA, 0) + ?.applicationInfo + isXposedModule = + appInfo != null && + ((appInfo.metaData?.containsKey("xposedminversion") == true) || + ConfigCache.getModuleApkPath(appInfo) != null) + } + + when (action) { + Intent.ACTION_PACKAGE_FULLY_REMOVED -> { + if (moduleName != null && + intent.getBooleanExtra("android.intent.extra.REMOVED_FOR_ALL_USERS", false)) { + if (ModuleDatabase.removeModule(moduleName)) isXposedModule = true + } + } + Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED -> { + if (isXposedModule && moduleName != null) { + val appInfo = + packageManager?.getPackageInfoCompat(moduleName, MATCH_ALL_FLAGS, 0)?.applicationInfo + if (appInfo != null) { + isXposedModule = + ModuleDatabase.updateModuleApkPath( + moduleName, ConfigCache.getModuleApkPath(appInfo), false) + } + } else if (ConfigCache.state.scopes.keys.any { it.uid == uid }) { + ConfigCache.requestCacheUpdate() + } + } + Intent.ACTION_UID_REMOVED -> { + if (isXposedModule) ConfigCache.requestCacheUpdate() + else if (ConfigCache.state.scopes.keys.any { it.uid == uid }) + ConfigCache.requestCacheUpdate() + } + } + + val removed = + action == Intent.ACTION_PACKAGE_FULLY_REMOVED || action == Intent.ACTION_UID_REMOVED + if (moduleName == BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME && userId == 0) { + Log.d(TAG, "Manager updated") + ConfigCache.updateManager(removed) + } + + // Broadcast back to Manager + if (moduleName != null) { + val notifyIntent = + Intent("org.lsposed.manager.NOTIFICATION").apply { + putExtra(Intent.EXTRA_INTENT, intent) + putExtra("android.intent.extra.PACKAGES", moduleName) + putExtra(Intent.EXTRA_USER, userId) + putExtra("isXposedModule", isXposedModule) + addFlags( + 0x01000000 or + 0x00400000) // FLAG_RECEIVER_INCLUDE_BACKGROUND | FLAG_RECEIVER_FROM_SHELL + setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + } + activityManager?.broadcastIntentCompat(notifyIntent) + } + } + + @Suppress("UNCHECKED_CAST") + private fun dispatchModuleScope(intent: Intent) { + val data = intent.data ?: return + val extras = intent.extras ?: return + val callbackBinder = extras.getBinder("callback") ?: return + if (!callbackBinder.isBinderAlive) return + + val authority = data.encodedAuthority ?: return + val parts = authority.split(":", limit = 2) + if (parts.size != 2) return + val packageName = parts[0] + val userId = parts[1].toIntOrNull() ?: return + + val scopePackageName = data.path?.substring(1) ?: return // remove leading '/' + val action = data.getQueryParameter("action") ?: return + + val iCallback = IXposedScopeCallback.Stub.asInterface(callbackBinder) + runCatching { + val appInfo = packageManager?.getPackageInfoCompat(scopePackageName, 0, userId) + if (appInfo == null) { + iCallback.onScopeRequestFailed("Package not found") + return + } + when (action) { + "approve" -> { + val scopes = ConfigCache.getModuleScope(packageName) ?: mutableListOf() + if (scopes.none { it.packageName == scopePackageName && it.userId == userId }) { + scopes.add( + org.lsposed.lspd.models.Application().apply { + this.packageName = scopePackageName + this.userId = userId + }) + ModuleDatabase.setModuleScope(packageName, scopes) + } + iCallback.onScopeRequestApproved(listOf(scopePackageName)) + } + "deny" -> iCallback.onScopeRequestFailed("Request denied by user") + "delete" -> iCallback.onScopeRequestFailed("Request timeout") + "block" -> { + val blocked = + PreferenceStore.getModulePrefs("lspd", 0, "config")["scope_request_blocked"] + as? Set ?: emptySet() + PreferenceStore.updateModulePref( + "lspd", 0, "config", "scope_request_blocked", blocked + packageName) + iCallback.onScopeRequestFailed("Request blocked by configuration") + } + } + } + .onFailure { runCatching { iCallback.onScopeRequestFailed(it.message) } } + + NotificationManager.cancelNotification( + NotificationManager.SCOPE_CHANNEL_ID, packageName, userId) + } +} 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 new file mode 100644 index 000000000..cfca60cc8 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -0,0 +1,442 @@ +package org.matrix.vector.daemon.data + +import android.content.pm.ApplicationInfo +import android.content.pm.PackageParser +import android.system.Os +import android.util.Log +import hidden.HiddenApiBridge +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.attribute.PosixFilePermissions +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.lsposed.lspd.models.Application +import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.ipc.InjectedModuleService +import org.matrix.vector.daemon.system.* +import org.matrix.vector.daemon.utils.getRealUsers + +private const val TAG = "VectorConfigCache" + +object ConfigCache { + // Module preference operations are delegated to PreferenceStore + // Writable operations of modules are delegated to ModuleDatabase + + @Volatile + var state = DaemonState() + private set + + val dbHelper = Database() // Kept public for PreferenceStore and ModuleDatabase + + private var miscPath: Path? = null + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val cacheUpdateChannel = Channel(Channel.CONFLATED) + + init { + scope.launch { + for (request in cacheUpdateChannel) { + performCacheUpdate() + } + } + initializeConfig() + } + + private fun initializeConfig() { + val config = PreferenceStore.getModulePrefs("lspd", 0, "config") + + // if (config["enable_auto_add_shortcut"] != null) { + // PreferenceStore.updateModulePref("lspd", 0, "config", "enable_auto_add_shortcut", null) + // } + + val pathStr = config["misc_path"] as? String + miscPath = + if (pathStr == null) { + val newPath = Paths.get("/data/misc", UUID.randomUUID().toString()) + PreferenceStore.updateModulePref("lspd", 0, "config", "misc_path", newPath.toString()) + newPath + } else { + Paths.get(pathStr) + } + + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + Files.createDirectories(miscPath!!, perms) + FileSystem.setSelinuxContextRecursive(miscPath!!, "u:object_r:xposed_data:s0") + } + .onFailure { Log.e(TAG, "Failed to create misc directory", it) } + } + + private fun ensureCacheReady() { + val currentState = state + if (!currentState.isCacheReady && packageManager?.asBinder()?.isBinderAlive == true) { + synchronized(this) { + if (!state.isCacheReady) { + Log.i(TAG, "System services are ready. Mapping modules and scopes.") + updateManager(false) + performCacheUpdate() + state = state.copy(isCacheReady = true) + } + } + } + } + + fun updateManager(uninstalled: Boolean) { + if (uninstalled) { + state = state.copy(managerUid = -1) + return + } + if (packageManager?.asBinder()?.isBinderAlive == true) { + runCatching { + val info = + packageManager?.getPackageInfoCompat(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0) + val uid = info?.applicationInfo?.uid ?: -1 + if (uid == -1) Log.i(TAG, "Manager is not installed") + state = state.copy(managerUid = uid) + } + .onFailure { state = state.copy(managerUid = -1) } + } + } + + fun isManager(uid: Int): Boolean { + ensureCacheReady() + return uid == state.managerUid || uid == BuildConfig.MANAGER_INJECTED_UID + } + + fun requestCacheUpdate() { + cacheUpdateChannel.trySend(Unit) + } + + /** Builds a completely new Immutable State and atomically swaps it. */ + private fun performCacheUpdate() { + if (packageManager == null) return + + Log.d(TAG, "Executing Cache Update...") + val db = dbHelper.readableDatabase + val oldState = state + + val newModules = mutableMapOf() + val obsoleteModules = mutableSetOf() + val obsoletePaths = mutableMapOf() + + db.query( + "modules", + arrayOf("module_pkg_name", "apk_path"), + "enabled = 1", + null, + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val pkgName = cursor.getString(0) + var apkPath = cursor.getString(1) + if (pkgName == "lspd") continue + + val oldModule = oldState.modules[pkgName] + + var pkgInfo: android.content.pm.PackageInfo? = null + val users = userManager?.getRealUsers() ?: emptyList() + for (user in users) { + pkgInfo = packageManager?.getPackageInfoCompat(pkgName, MATCH_ALL_FLAGS, user.id) + if (pkgInfo?.applicationInfo != null) break + } + + if (pkgInfo?.applicationInfo == null) { + Log.w(TAG, "Failed to find package info of $pkgName") + obsoleteModules.add(pkgName) + continue + } + + val appInfo = pkgInfo.applicationInfo + + if (oldModule != null && + appInfo?.sourceDir != null && + apkPath != null && + oldModule.apkPath != null && + FileSystem.toGlobalNamespace(apkPath).exists() && + apkPath == oldModule.apkPath && + File(appInfo.sourceDir).parent == File(apkPath).parent) { + + if (oldModule.appId == -1) oldModule.applicationInfo = appInfo + newModules[pkgName] = oldModule + continue + } + + val realApkPath = getModuleApkPath(appInfo!!) + if (realApkPath == null) { + Log.w(TAG, "Failed to find path of $pkgName") + obsoleteModules.add(pkgName) + continue + } else { + apkPath = realApkPath + obsoletePaths[pkgName] = realApkPath + } + + val preLoadedApk = + FileSystem.loadModule(apkPath, PreferenceStore.isDexObfuscateEnabled()) + if (preLoadedApk != null) { + val module = + Module().apply { + packageName = pkgName + this.apkPath = apkPath + appId = appInfo.uid + applicationInfo = appInfo + service = oldModule?.service ?: InjectedModuleService(pkgName) + file = preLoadedApk + } + newModules[pkgName] = module + } else { + Log.w(TAG, "Failed to parse DEX/ZIP for $pkgName, skipping.") + obsoleteModules.add(pkgName) + } + } + } + + if (packageManager?.asBinder()?.isBinderAlive == true) { + obsoleteModules.forEach { ModuleDatabase.removeModule(it) } + obsoletePaths.forEach { (pkg, path) -> ModuleDatabase.updateModuleApkPath(pkg, path, true) } + } + + val newScopes = mutableMapOf>() + db.query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "module_pkg_name", "user_id"), + "enabled = 1", + null, + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val appPkg = cursor.getString(0) + val modPkg = cursor.getString(1) + val userId = cursor.getInt(2) + + if (appPkg == "system") continue + + val module = newModules[modPkg] ?: continue + val pkgInfo = + packageManager?.getPackageInfoWithComponents(appPkg, MATCH_ALL_FLAGS, userId) + if (pkgInfo?.applicationInfo == null) continue + + val processNames = pkgInfo.fetchProcesses() + if (processNames.isEmpty()) continue + + val appUid = pkgInfo.applicationInfo!!.uid + + for (processName in processNames) { + val processScope = ProcessScope(processName, appUid) + newScopes.getOrPut(processScope) { mutableListOf() }.add(module) + + if (modPkg == appPkg) { + val appId = appUid % PER_USER_RANGE + userManager?.getRealUsers()?.forEach { user -> + val moduleUid = user.id * PER_USER_RANGE + appId + if (moduleUid != appUid) { + val moduleSelf = ProcessScope(processName, moduleUid) + newScopes.getOrPut(moduleSelf) { mutableListOf() }.add(module) + } + } + } + } + } + } + + // --- ATOMIC STATE SWAP --- + state = oldState.copy(modules = newModules, scopes = newScopes) + + Log.d(TAG, "Cache Update Complete. Map Swap successful.") + // Log.d(TAG, "cached modules:") + // newModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") } + + // Log.d(TAG, "cached scopes:") + // newScopes.forEach { (ps, modules) -> + // Log.d(TAG, "${ps.processName}/${ps.uid}") + // modules.forEach { mod -> Log.d(TAG, "\t${mod.packageName}") } + // } + } + + fun getModuleScope(packageName: String): MutableList? { + if (packageName == "lspd") return null + val result = mutableListOf() + ConfigCache.dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "user_id"), + "modules.module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + result.add( + Application().apply { + this.packageName = cursor.getString(0) + this.userId = cursor.getInt(1) + }) + } + } + return result + } + + fun getAutoInclude(packageName: String): Boolean { + if (packageName == "lspd") return false + + var isAutoInclude = false + ConfigCache.dbHelper.readableDatabase + .query( + "modules", + arrayOf("auto_include"), + "module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + if (cursor.moveToFirst()) { + isAutoInclude = cursor.getInt(0) == 1 + } + } + return isAutoInclude + } + + fun getModulesForProcess(processName: String, uid: Int): List { + ensureCacheReady() + return state.scopes[ProcessScope(processName, uid)] ?: emptyList() + } + + fun getModuleByUid(uid: Int): Module? = + state.modules.values.firstOrNull { it.appId == uid % PER_USER_RANGE } + + fun getModulesForSystemServer(): List { + val modules = mutableListOf() + if (!android.os.SELinux.checkSELinuxAccess( + "u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { + Log.e(TAG, "Skipping system_server injection: sepolicy execmem denied") + return modules + } + + val currentState = state + + dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("module_pkg_name", "apk_path"), + "app_pkg_name=? AND enabled=1", + arrayOf("system"), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val pkgName = cursor.getString(0) + val apkPath = cursor.getString(1) + + val cached = currentState.modules[pkgName] + if (cached != null) { + modules.add(cached) + continue + } + + val statPath = FileSystem.toGlobalNamespace("/data/user_de/0/$pkgName").absolutePath + val module = + Module().apply { + packageName = pkgName + this.apkPath = apkPath + appId = runCatching { Os.stat(statPath).st_uid }.getOrDefault(-1) + service = InjectedModuleService(pkgName) + } + + runCatching { + @Suppress("DEPRECATION") + val pkg = PackageParser().parsePackage(File(apkPath), 0, false) + module.applicationInfo = pkg.applicationInfo + } + .onFailure { + Log.w(TAG, "PackageParser failed for $apkPath, using fallback ApplicationInfo") + module.applicationInfo = ApplicationInfo().apply { packageName = pkgName } + } + + // Always apply the critical paths manually, even on fallback + module.applicationInfo?.apply { + sourceDir = apkPath + dataDir = statPath + deviceProtectedDataDir = statPath + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(this, statPath) + processName = pkgName + uid = module.appId + } + + FileSystem.loadModule(apkPath, PreferenceStore.isDexObfuscateEnabled())?.let { + module.file = it + modules.add(module) + // We intentionally don't mutate state.modules here. Cache update will catch it. + } + } + } + return modules + } + + fun getModuleApkPath(info: ApplicationInfo): String? { + val apks = mutableListOf() + info.sourceDir?.let { apks.add(it) } + info.splitSourceDirs?.let { apks.addAll(it) } + + return apks.firstOrNull { apk -> + runCatching { + java.util.zip.ZipFile(apk).use { zip -> + zip.getEntry("META-INF/xposed/java_init.list") != null || + zip.getEntry("assets/xposed_init") != null + } + } + .getOrDefault(false) + } + } + + fun getInstalledModules(): List { + val allPackages = + packageManager?.getInstalledPackagesFromAllUsers(MATCH_ALL_FLAGS, false) ?: emptyList() + return allPackages + .mapNotNull { it.applicationInfo } + .filter { info -> getModuleApkPath(info) != null } + } + + fun shouldSkipProcess(scope: ProcessScope): Boolean { + ensureCacheReady() + return !state.scopes.containsKey(scope) && !isManager(scope.uid) + } + + fun getPrefsPath(packageName: String, uid: Int): String { + ensureCacheReady() + val currentState = state + val basePath = miscPath ?: throw IllegalStateException("Fatal: miscPath not initialized!") + + val userId = uid / PER_USER_RANGE + val userSuffix = if (userId == 0) "" else userId.toString() + val path = basePath.resolve("prefs$userSuffix").resolve(packageName) + + val module = currentState.modules[packageName] + if (module != null && module.appId == uid % PER_USER_RANGE) { + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + Files.createDirectories(path, perms) + Files.walk(path).forEach { p -> Os.chown(p.toString(), uid, uid) } + } + .onFailure { Log.e(TAG, "Failed to prepare prefs path", it) } + } + return path.toString() + } + + fun getDenyListPackages(): List = emptyList() // TODO: implement it +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt new file mode 100644 index 000000000..7e01f7521 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt @@ -0,0 +1,16 @@ +package org.matrix.vector.daemon.data + +import org.lsposed.lspd.models.Module + +data class ProcessScope(val processName: String, val uid: Int) + +/** + * An immutable snapshot of the Daemon's state. Any updates will generate a new copy of this class + * and atomically swap the reference. + */ +data class DaemonState( + val managerUid: Int = -1, + val isCacheReady: Boolean = false, + val modules: Map = emptyMap(), + val scopes: Map> = emptyMap() +) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt new file mode 100644 index 000000000..b3d21e60d --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt @@ -0,0 +1,93 @@ +package org.matrix.vector.daemon.data + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import org.matrix.vector.daemon.utils.FakeContext + +private const val TAG = "VectorDatabase" +private const val DB_VERSION = 4 + +class Database(context: Context? = FakeContext()) : + SQLiteOpenHelper(context, FileSystem.dbPath.absolutePath, null, DB_VERSION) { + + override fun onConfigure(db: SQLiteDatabase) { + super.onConfigure(db) + db.setForeignKeyConstraintsEnabled(true) + db.enableWriteAheadLogging() + // Improve write performance + db.execSQL("PRAGMA synchronous=NORMAL;") + } + + override fun onCreate(db: SQLiteDatabase) { + Log.i(TAG, "Creating new Vector database") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS modules ( + mid integer PRIMARY KEY AUTOINCREMENT, + module_pkg_name text NOT NULL UNIQUE, + apk_path text NOT NULL, + enabled BOOLEAN DEFAULT 0 CHECK (enabled IN (0, 1)), + auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1)) + ); + """ + .trimIndent()) + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS scope ( + mid integer, + app_pkg_name text NOT NULL, + user_id integer NOT NULL, + PRIMARY KEY (mid, app_pkg_name, user_id), + CONSTRAINT scope_module_constraint FOREIGN KEY (mid) REFERENCES modules (mid) ON DELETE CASCADE + ); + """ + .trimIndent()) + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS configs ( + module_pkg_name text NOT NULL, + user_id integer NOT NULL, + `group` text NOT NULL, + `key` text NOT NULL, + data blob NOT NULL, + PRIMARY KEY (module_pkg_name, user_id, `group`, `key`), + CONSTRAINT config_module_constraint FOREIGN KEY (module_pkg_name) REFERENCES modules (module_pkg_name) ON DELETE CASCADE + ); + """ + .trimIndent()) + + db.execSQL("CREATE INDEX IF NOT EXISTS configs_idx ON configs (module_pkg_name, user_id);") + + // Insert self + db.execSQL( + "INSERT OR IGNORE INTO modules (module_pkg_name, apk_path) VALUES ('lspd', ?)", + arrayOf(FileSystem.managerApkPath.toString())) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + Log.i(TAG, "Upgrading database from $oldVersion to $newVersion") + if (oldVersion < 2) { + db.execSQL("DROP INDEX IF EXISTS configs_idx;") + db.execSQL("ALTER TABLE scope RENAME TO old_scope;") + db.execSQL("ALTER TABLE configs RENAME TO old_configs;") + onCreate(db) // Recreate tables with strict constraints + runCatching { db.execSQL("INSERT INTO scope SELECT * FROM old_scope;") } + runCatching { db.execSQL("INSERT INTO configs SELECT * FROM old_configs;") } + db.execSQL("DROP TABLE old_scope;") + db.execSQL("DROP TABLE old_configs;") + } + if (oldVersion < 3) { + db.execSQL("UPDATE scope SET app_pkg_name = 'system' WHERE app_pkg_name = 'android';") + } + if (oldVersion < 4) { + runCatching { + db.execSQL( + "ALTER TABLE modules ADD COLUMN auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1));") + } + } + } +} 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 new file mode 100644 index 000000000..8ac061ce7 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -0,0 +1,415 @@ +package org.matrix.vector.daemon.data + +import android.content.res.AssetManager +import android.content.res.Resources +import android.os.Binder +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.RemoteException +import android.os.SELinux +import android.os.SharedMemory +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import hidden.HiddenApiBridge +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.nio.channels.Channels +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.nio.file.attribute.PosixFilePermissions +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import org.lsposed.lspd.models.PreLoadedApk +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.utils.ObfuscationManager + +private const val TAG = "VectorFileSystem" + +object FileSystem { + val basePath: Path = Paths.get("/data/adb/lspd") + val logDirPath: Path = basePath.resolve("log") + val oldLogDirPath: Path = basePath.resolve("log.old") + val modulePath: Path = basePath.resolve("modules") + val socketPath: Path = basePath.resolve(".cli_sock") + val daemonApkPath: Path = Paths.get(System.getProperty("java.class.path", "")) + val managerApkPath: Path = daemonApkPath.parent.resolve("manager.apk") + val configDirPath: Path = basePath.resolve("config") + val dbPath: File = configDirPath.resolve("modules_config.db").toFile() + val magiskDbPath = File("/data/adb/magisk.db") + + @Volatile private var preloadDex: SharedMemory? = null + + private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()) + private val lockPath: Path = basePath.resolve("lock") + private var fileLock: FileLock? = null + private var lockChannel: FileChannel? = null + + init { + runCatching { + Files.createDirectories(basePath) + Os.chmod(basePath.toString(), "700".toInt(8)) + SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0") + Files.createDirectories(configDirPath) + } + .onFailure { Log.e(TAG, "Failed to initialize directories", it) } + } + + fun setupCli(): String { + val cliSource = daemonApkPath.parent.resolve("cli").toFile() + val cliDest = basePath.resolve("cli").toFile() + if (cliSource.exists()) { + runCatching { + cliSource.copyTo(cliDest, overwrite = true) + Os.chmod(cliDest.absolutePath, "700".toInt(8)) + } + .onFailure { Log.e(TAG, "Failed to deploy CLI script", it) } + } + + val cliSocket: String = socketPath.toString() + val socketFile = File(cliSocket) + if (socketFile.exists()) { + Log.d(TAG, "Existing $cliSocket deleted") + socketFile.delete() + } + + return cliSocket + } + + /** Tries to lock the daemon lockfile. Returns false if another daemon is running. */ + fun tryLock(): Boolean { + return runCatching { + val permissions = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")) + lockChannel = + FileChannel.open( + lockPath, setOf(StandardOpenOption.CREATE, StandardOpenOption.WRITE), permissions) + fileLock = lockChannel?.tryLock() + fileLock?.isValid == true + } + .getOrDefault(false) + } + + /** Clears all special file attributes (like immutable) on a directory. */ + fun chattr0(path: Path): Boolean { + return runCatching { + val fd = Os.open(path.toString(), OsConstants.O_RDONLY, 0) + // 0x40086602 for 64-bit, 0x40046602 for 32-bit (FS_IOC_SETFLAGS) + val req = if (Process.is64Bit()) 0x40086602 else 0x40046602 + HiddenApiBridge.Os_ioctlInt(fd, req, 0) + Os.close(fd) + true + } + .recover { e -> if (e is ErrnoException && e.errno == OsConstants.ENOTSUP) true else false } + .getOrDefault(false) + } + + /** Recursively sets SELinux context. Crucial for modules to read their data. */ + fun setSelinuxContextRecursive(path: Path, context: String) { + runCatching { + SELinux.setFileContext(path.toString(), context) + if (path.isDirectory()) { + Files.list(path).use { stream -> + stream.forEach { setSelinuxContextRecursive(it, context) } + } + } + } + .onFailure { Log.e(TAG, "Failed to set SELinux context for $path", it) } + } + + /** + * Lazily loads resources from the daemon's APK path via reflection. This allows FakeContext to + * access strings/drawables without a real application context. + */ + val resources: Resources by lazy { + val am = AssetManager::class.java.getDeclaredConstructor().newInstance() + val addAssetPath = + AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java).apply { + isAccessible = true + } + addAssetPath.invoke(am, daemonApkPath.toString()) + @Suppress("DEPRECATION") Resources(am, null, null) + } + + /** Loads a single DEX file into SharedMemory, optionally applying obfuscation. */ + private fun readDex(inputStream: InputStream, obfuscate: Boolean): SharedMemory { + var memory = SharedMemory.create(null, inputStream.available()) + val byteBuffer = memory.mapReadWrite() + Channels.newChannel(inputStream).read(byteBuffer) + SharedMemory.unmap(byteBuffer) + + if (obfuscate) { + val newMemory = ObfuscationManager.obfuscateDex(memory) + if (memory !== newMemory) { + memory.close() + memory = newMemory + } + } + memory.setProtect(OsConstants.PROT_READ) + return memory + } + + /** Parses the module APK, extracts init lists, and loads DEXes into SharedMemory. */ + fun loadModule(apkPath: String, obfuscate: Boolean): PreLoadedApk? { + val file = File(apkPath) + if (!file.exists()) return null + + val preLoadedApk = PreLoadedApk() + val preLoadedDexes = mutableListOf() + val moduleClassNames = mutableListOf() + val moduleLibraryNames = mutableListOf() + var isLegacy = false + + runCatching { + ZipFile(file).use { zip -> + // Read all classes*.dex files + var secondary = 1 + while (true) { + val entryName = if (secondary == 1) "classes.dex" else "classes$secondary.dex" + val dexEntry = zip.getEntry(entryName) ?: break + zip.getInputStream(dexEntry).use { preLoadedDexes.add(readDex(it, obfuscate)) } + secondary++ + } + + // Read initialization lists + fun readList(name: String, dest: MutableList) { + zip.getEntry(name)?.let { entry -> + zip.getInputStream(entry).bufferedReader().useLines { lines -> + lines + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + .forEach { dest.add(it) } + } + } + } + + readList("META-INF/xposed/java_init.list", moduleClassNames) + if (moduleClassNames.isEmpty()) { + isLegacy = true + readList("assets/xposed_init", moduleClassNames) + readList("assets/native_init", moduleLibraryNames) + } else { + readList("META-INF/xposed/native_init.list", moduleLibraryNames) + } + } + } + .onFailure { + Log.e(TAG, "Failed to load module $apkPath", it) + return null + } + + if (preLoadedDexes.isEmpty() || moduleClassNames.isEmpty()) return null + + // 3. Apply obfuscation to class names if required + if (obfuscate) { + val signatures = ObfuscationManager.getSignatures() + for (i in moduleClassNames.indices) { + val s = moduleClassNames[i] + signatures.entries + .firstOrNull { s.startsWith(it.key) } + ?.let { moduleClassNames[i] = s.replace(it.key, it.value) } + } + } + + preLoadedApk.preLoadedDexes = preLoadedDexes + preLoadedApk.moduleClassNames = moduleClassNames + preLoadedApk.moduleLibraryNames = moduleLibraryNames + preLoadedApk.legacy = isLegacy + + return preLoadedApk + } + + /** Safely creates the log directory. If a file exists with the same name, it deletes it first. */ + private fun createLogDirPath() { + if (!Files.isDirectory(logDirPath, java.nio.file.LinkOption.NOFOLLOW_LINKS)) { + logDirPath.toFile().deleteRecursively() + } + Files.createDirectories(logDirPath) + } + + /** + * Rotates the log directory by clearing file attributes (chattr 0), deleting the old backup, and + * renaming the current log directory to the backup. + */ + fun moveLogDir() { + runCatching { + if (Files.exists(logDirPath)) { + if (chattr0(logDirPath)) { + // Kotlin's deleteRecursively replaces the verbose Java SimpleFileVisitor + oldLogDirPath.toFile().deleteRecursively() + Files.move(logDirPath, oldLogDirPath) + } + } + Files.createDirectories(logDirPath) + } + .onFailure { Log.e(TAG, "Failed to move log directory", it) } + } + + fun getPropsPath(): File { + createLogDirPath() + return logDirPath.resolve("props.txt").toFile() + } + + fun getKmsgPath(): File { + createLogDirPath() + return logDirPath.resolve("kmsg.log").toFile() + } + + @Synchronized + fun getPreloadDex(obfuscate: Boolean): SharedMemory? { + if (preloadDex == null) { + runCatching { + FileInputStream("framework/lspd.dex").use { preloadDex = readDex(it, obfuscate) } + } + .onFailure { Log.e(TAG, "Failed to load framework dex", it) } + } + return preloadDex + } + + fun ensureModuleFilePath(path: String?) { + if (path == null || path.contains(File.separatorChar) || path == "." || path == "..") { + throw RemoteException("Invalid path: $path") + } + } + + fun resolveModuleDir(packageName: String, dir: String, userId: Int, uid: Int): Path { + val path = modulePath.resolve(userId.toString()).resolve(packageName).resolve(dir).normalize() + path.toFile().mkdirs() + + if (SELinux.getFileContext(path.toString()) != "u:object_r:xposed_data:s0") { + runCatching { + setSelinuxContextRecursive(path, "u:object_r:xposed_data:s0") + if (uid != -1) Os.chown(path.toString(), uid, uid) + Os.chmod(path.toString(), 0x1ed) // 0755 + } + .onFailure { throw RemoteException("Failed to set SELinux context: ${it.message}") } + } + return path + } + + fun toGlobalNamespace(path: String): File { + return if (path.startsWith("/")) File("/proc/1/root", path) else File("/proc/1/root/$path") + } + + fun getLogs(zipFd: ParcelFileDescriptor) { + runCatching { + ZipOutputStream(java.io.FileOutputStream(zipFd.fileDescriptor)).use { os -> + val comment = + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + os.setComment(comment) + os.setLevel(java.util.zip.Deflater.BEST_COMPRESSION) + + fun addFile(name: String, file: File) { + if (!file.exists() || !file.isFile) return + runCatching { + os.putNextEntry(ZipEntry(name)) + file.inputStream().use { it.copyTo(os) } + os.closeEntry() + } + } + + fun addDir(basePath: String, dir: File) { + if (!dir.exists() || !dir.isDirectory) return + // Kotlin's walkTopDown elegantly replaces Files.walkFileTree + dir.walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relativePath = dir.toPath().relativize(file.toPath()).toString() + val entryName = + if (basePath.isEmpty()) relativePath else "$basePath/$relativePath" + addFile(entryName, file) + } + } + + fun addProcOutput(name: String, vararg cmd: String) { + runCatching { + val proc = ProcessBuilder(*cmd).start() + os.putNextEntry(ZipEntry(name)) + proc.inputStream.use { it.copyTo(os) } + os.closeEntry() + } + } + + // 1. Gather daemon logs and system crash traces + addDir("log", logDirPath.toFile()) + addDir("log.old", oldLogDirPath.toFile()) + addDir("tombstones", File("/data/tombstones")) + addDir("anr", File("/data/anr")) + addDir( + "crash1", File("/data/data/${BuildConfig.MANAGER_INJECTED_PKG_NAME}/cache/crash")) + addDir( + "crash2", + File("/data/data/${BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME}/cache/crash")) + + // 2. Gather system logs directly via shell + addProcOutput("full.log", "logcat", "-b", "all", "-d") + addProcOutput("dmesg.log", "dmesg") + + // 3. Gather Magisk module states safely + val magiskDataDir = File("/data/adb/modules") + if (magiskDataDir.exists() && magiskDataDir.isDirectory) { + magiskDataDir.listFiles()?.forEach { moduleDir -> + val modName = moduleDir.name + listOf("module.prop", "remove", "disable", "update", "sepolicy.rule").forEach { + addFile("modules/$modName/$it", File(moduleDir, it)) + } + } + } + + // 4. Gather memory/mount info for daemon and caller + val proc = File("/proc") + arrayOf("self", Binder.getCallingPid().toString()).forEach { pid -> + val pidPath = File(proc, pid) + listOf("maps", "mountinfo", "status").forEach { + addFile("proc/$pid/$it", File(pidPath, it)) + } + } + + // 5. Gather Database and Scopes + addFile("modules_config.db", dbPath) + runCatching { + os.putNextEntry(ZipEntry("scopes.txt")) + ConfigCache.state.scopes.forEach { (scope, modules) -> + os.write("${scope.processName}/${scope.uid}\n".toByteArray()) + modules.forEach { mod -> + os.write("\t${mod.packageName}\n".toByteArray()) + mod.file?.moduleClassNames?.forEach { cn -> os.write("\t\t$cn\n".toByteArray()) } + mod.file?.moduleLibraryNames?.forEach { ln -> + os.write("\t\t$ln\n".toByteArray()) + } + } + } + os.closeEntry() + } + } + } + .onFailure { Log.e(TAG, "Failed to export logs", it) } + .also { runCatching { zipFd.close() } } + } + + private fun getNewLogFileName(prefix: String): String { + return "${prefix}_${formatter.format(Instant.now())}.log" + } + + fun getNewVerboseLogPath(): File { + createLogDirPath() + return logDirPath.resolve(getNewLogFileName("verbose")).toFile() + } + + fun getNewModulesLogPath(): File { + createLogDirPath() + return logDirPath.resolve(getNewLogFileName("modules")).toFile() + } +} 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 new file mode 100644 index 000000000..cfeca4165 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt @@ -0,0 +1,146 @@ +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 + +private const val TAG = "VectorModuleDb" + +object ModuleDatabase { + + fun enableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val db = ConfigCache.dbHelper.writableDatabase + var changed = false + + // First, check if it exists. If not, we need to "discover" it. + val exists = + db.compileStatement("SELECT COUNT(*) FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() > 0 + if (!exists) { + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", "") // defer to cache updating + put("enabled", 1) + } + db.insert("modules", null, values) + changed = true + } else { + val values = ContentValues().apply { put("enabled", 1) } + changed = db.update("modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + } + + if (changed) ConfigCache.requestCacheUpdate() + return changed + } + + fun disableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val values = ContentValues().apply { put("enabled", 0) } + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (changed) ConfigCache.requestCacheUpdate() + return changed + } + + fun setModuleScope(packageName: String, scope: MutableList): Boolean { + enableModule(packageName) + val db = ConfigCache.dbHelper.writableDatabase + db.beginTransaction() + try { + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete("scope", "mid = ?", arrayOf(mid.toString())) + + val values = ContentValues().apply { put("mid", mid) } + for (app in scope) { + if (app.packageName == "system" && app.userId != 0) continue + values.put("app_pkg_name", app.packageName) + values.put("user_id", app.userId) + db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE) + } + db.setTransactionSuccessful() + } catch (e: Exception) { + Log.e(TAG, "Failed to set scope", e) + return false + } finally { + db.endTransaction() + } + ConfigCache.requestCacheUpdate() + return true + } + + fun removeModuleScope(packageName: String, scopePackageName: String, userId: Int): Boolean { + if (packageName == "lspd" || (scopePackageName == "system" && userId != 0)) return false + val db = ConfigCache.dbHelper.writableDatabase + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete( + "scope", + "mid = ? AND app_pkg_name = ? AND user_id = ?", + arrayOf(mid.toString(), scopePackageName, userId.toString())) + ConfigCache.requestCacheUpdate() + return true + } + + fun updateModuleApkPath(packageName: String, apkPath: String?, force: Boolean): Boolean { + if (apkPath == null || packageName == "lspd") return false + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", apkPath) + } + val db = ConfigCache.dbHelper.writableDatabase + var count = + db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt() + + if (count < 0) { + val cached = ConfigCache.state.modules[packageName] + if (force || cached == null || cached.apkPath != apkPath) { + count = + db.updateWithOnConflict( + "modules", + values, + "module_pkg_name=?", + arrayOf(packageName), + SQLiteDatabase.CONFLICT_IGNORE) + } else count = 0 + } + if (!force && count > 0) ConfigCache.requestCacheUpdate() + return count > 0 + } + + fun removeModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val res = + ConfigCache.dbHelper.writableDatabase.delete( + "modules", "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (res) ConfigCache.requestCacheUpdate() + return res + } + + fun setAutoInclude(packageName: String, enabled: Boolean): Boolean { + if (packageName == "lspd") return false + + val values = ContentValues().apply { put("auto_include", if (enabled) 1 else 0) } + + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + + // If the auto_include flag changes, we should rebuild the scope cache + if (changed) { + ConfigCache.requestCacheUpdate() + } + + return changed + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt new file mode 100644 index 000000000..70760d00d --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt @@ -0,0 +1,104 @@ +package org.matrix.vector.daemon.data + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import org.apache.commons.lang3.SerializationUtilsX + +object PreferenceStore { + + fun getModulePrefs(packageName: String, userId: Int, group: String): Map { + val result = mutableMapOf() + val db = + runCatching { + SQLiteDatabase.openDatabase( + FileSystem.dbPath.absolutePath, null, SQLiteDatabase.OPEN_READONLY) + } + .getOrNull() ?: return result + db.use { + it.query( + "configs", + arrayOf("`key`", "data"), + "module_pkg_name = ? AND user_id = ? AND `group` = ?", + arrayOf(packageName, userId.toString(), group), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val key = cursor.getString(0) + val blob = cursor.getBlob(1) + val obj = SerializationUtilsX.deserialize(blob) + if (obj != null) result[key] = obj + } + } + } + return result + } + + fun updateModulePref(moduleName: String, userId: Int, group: String, key: String, value: Any?) { + updateModulePrefs(moduleName, userId, group, mapOf(key to value)) + } + + fun updateModulePrefs(moduleName: String, userId: Int, group: String, diff: Map) { + val db = ConfigCache.dbHelper.writableDatabase + db.beginTransaction() + try { + for ((key, value) in diff) { + if (value is java.io.Serializable) { + val values = + ContentValues().apply { + put("`group`", group) + put("`key`", key) + put("data", SerializationUtilsX.serialize(value)) + put("module_pkg_name", moduleName) + put("user_id", userId.toString()) + } + db.insertWithOnConflict("configs", null, values, SQLiteDatabase.CONFLICT_REPLACE) + } else { + db.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=? AND `key`=?", + arrayOf(moduleName, userId.toString(), group, key)) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun deleteModulePrefs(moduleName: String, userId: Int, group: String) { + ConfigCache.dbHelper.writableDatabase.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=?", + arrayOf(moduleName, userId.toString(), group)) + } + + fun isDexObfuscateEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_dex_obfuscate"] as? Boolean ?: true + + fun setDexObfuscate(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_dex_obfuscate", enabled) + + fun isLogWatchdogEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_log_watchdog"] as? Boolean ?: true + + fun setLogWatchdog(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_log_watchdog", enabled) + + fun isStatusNotificationEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_status_notification"] as? Boolean ?: true + + fun setStatusNotification(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_status_notification", enabled) + + fun isVerboseLogEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_verbose_log"] as? Boolean ?: true + + fun setVerboseLog(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) + + fun isScopeRequestBlocked(pkg: String): Boolean = + (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == + true +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt new file mode 100644 index 000000000..3ea1a507a --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt @@ -0,0 +1,129 @@ +package org.matrix.vector.daemon.env + +import android.net.LocalServerSocket +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.system.Os +import android.util.Log +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import org.matrix.vector.daemon.* +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.ipc.CliHandler + +private const val TAG = "VectorCliSever" + +object CliSocketServer { + + private var isRunning = false + + fun start() { + if (isRunning) return + isRunning = true + + val serverThread = Thread { + // Keep these references outside the loop to prevent GC from closing them + var rootSocket: LocalSocket? = null + var server: LocalServerSocket? = null + var socketFile: File? = null + + try { + val cliSocketPath: String = FileSystem.setupCli() + socketFile = File(cliSocketPath) + + // Create a standard LocalSocket + rootSocket = LocalSocket() + // Bind it to the filesystem path + val address = LocalSocketAddress(cliSocketPath, LocalSocketAddress.Namespace.FILESYSTEM) + rootSocket.bind(address) + + // LocalServerSocket(FileDescriptor) requires the FD to already be listening. + Os.listen(rootSocket.fileDescriptor, 50) + // Wrap the underlying FileDescriptor into a ServerSocket + server = LocalServerSocket(rootSocket.fileDescriptor) + + Log.d(TAG, "CLI server started at $cliSocketPath") + + while (!Thread.currentThread().isInterrupted) { + try { + val clientSocket = server.accept() + handleClient(clientSocket) + } catch (e: IOException) { + if (Thread.currentThread().isInterrupted) break + Log.w(TAG, "Error accepting client", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "Fatal CLI Server error", e) + } finally { + try { + server?.close() + rootSocket?.close() + } catch (ignored: Exception) {} + + if (socketFile?.exists() == true) { + socketFile.delete() + } + isRunning = false + Log.d(TAG, "CLI server stopped") + } + } + + serverThread.name = "VectorCliListener" + serverThread.priority = Thread.MIN_PRIORITY + serverThread.start() + } + + private fun handleClient(socket: LocalSocket) { + try { + val input = DataInputStream(socket.inputStream) + val output = DataOutputStream(socket.outputStream) + + // Read & Verify Security Token (UUID MSB/LSB) + val msb = input.readLong() + val lsb = input.readLong() + if (msb != BuildConfig.CLI_TOKEN_MSB || lsb != BuildConfig.CLI_TOKEN_LSB) { + socket.close() + return + } + + val requestJson = input.readUTF() + val request = VectorIPC.gson.fromJson(requestJson, CliRequest::class.java) + + // Intercept Log Streaming specifically before CliHandler + if (request.command == "log" && request.action == "stream") { + val verbose = request.options["verbose"] as? Boolean ?: false + val logFile = if (verbose) LogcatMonitor.getVerboseLog() else LogcatMonitor.getModulesLog() + + if (logFile != null && logFile.exists()) { + val response = CliResponse(success = true, isFdAttached = true) + output.writeUTF(VectorIPC.gson.toJson(response)) + + // Open file and get raw FileDescriptor + val fis = FileInputStream(logFile) + val fd = fis.fd + + // Attach FD to the next write operation + socket.setFileDescriptorsForSend(arrayOf(fd)) + output.write(1) // Trigger byte to "carry" the ancillary FD data + + // fis is closed when the socket/method finishes + return + } else { + output.writeUTF( + VectorIPC.gson.toJson(CliResponse(success = false, error = "Log file not found."))) + return + } + } + + // Standard commands go to CliHandler as usual + val response = CliHandler.execute(request) + output.writeUTF(VectorIPC.gson.toJson(response)) + } finally { + socket.close() + } + } +} 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 new file mode 100644 index 000000000..f8563b1ec --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt @@ -0,0 +1,248 @@ +package org.matrix.vector.daemon.env + +import android.net.LocalServerSocket +import android.os.Build +import android.os.FileObserver +import android.os.SELinux +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import java.io.File +import java.io.FileDescriptor +import java.io.FileInputStream +import java.nio.file.Files +import java.nio.file.Paths +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +private const val TAG = "VectorDex2Oat" + +// Compatibility states matching Manager expectations +const val DEX2OAT_OK = 0 +const val DEX2OAT_MOUNT_FAILED = 1 +const val DEX2OAT_SEPOLICY_INCORRECT = 2 +const val DEX2OAT_SELINUX_PERMISSIVE = 3 +const val DEX2OAT_CRASHED = 4 + +object Dex2OatServer { + private const val WRAPPER32 = "bin/dex2oat32" + private const val WRAPPER64 = "bin/dex2oat64" + private const val HOOKER32 = "bin/liboat_hook32.so" + private const val HOOKER64 = "bin/liboat_hook64.so" + + private val dex2oatArray = arrayOfNulls(6) + private val fdArray = arrayOfNulls(6) + + @Volatile + var compatibility = DEX2OAT_OK + private set + + private external fun doMountNative( + enabled: Boolean, + r32: String?, + d32: String?, + r64: String?, + d64: String? + ) + + private external fun setSockCreateContext(context: String?): Boolean + + private external fun getSockPath(): String + + private val selinuxObserver = + object : + FileObserver( + listOf(File("/sys/fs/selinux/enforce"), File("/sys/fs/selinux/policy")), + CLOSE_WRITE) { + override fun onEvent(event: Int, path: String?) { + synchronized(this) { + if (compatibility == DEX2OAT_CRASHED) { + stopWatching() + return + } + + val enforcing = + runCatching { + Files.newInputStream(Paths.get("/sys/fs/selinux/enforce")).use { + it.read() == '1'.code + } + } + .getOrDefault(false) + + when { + !enforcing -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SELINUX_PERMISSIVE + } + hasSePolicyErrors() -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SEPOLICY_INCORRECT + } + compatibility != DEX2OAT_OK -> { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + stopWatching() + } else { + compatibility = DEX2OAT_OK + } + } + } + } + } + } + + init { + // Android 10 vs 11+ path differences + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64") + } else { + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64") + } + + openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so") + openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so") + } + + private fun hasSePolicyErrors(): Boolean { + return SELinux.checkSELinuxAccess( + "u:r:untrusted_app:s0", "u:object_r:dex2oat_exec:s0", "file", "execute") || + SELinux.checkSELinuxAccess( + "u:r:untrusted_app:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans") + } + + private fun openDex2oat(id: Int, path: String) { + runCatching { + fdArray[id] = Os.open(path, OsConstants.O_RDONLY, 0) + dex2oatArray[id] = path + } + } + + private fun checkAndAddDex2Oat(path: String) { + val file = File(path) + if (!file.exists()) return + + runCatching { + FileInputStream(file).use { fis -> + val header = ByteArray(5) + if (fis.read(header) != 5) return + // Verify ELF Magic: 0x7F 'E' 'L' 'F' + if (header[0] != 0x7F.toByte() || + header[1] != 'E'.code.toByte() || + header[2] != 'L'.code.toByte() || + header[3] != 'F'.code.toByte()) + return + + val is32Bit = header[4] == 1.toByte() + val is64Bit = header[4] == 2.toByte() + val isDebug = path.contains("dex2oatd") + + val index = + when { + is32Bit -> if (isDebug) 1 else 0 + is64Bit -> if (isDebug) 3 else 2 + else -> -1 + } + + if (index != -1 && dex2oatArray[index] == null) { + dex2oatArray[index] = path + fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0) + Log.i(TAG, "Detected $path -> Assigned Index $index") + } + } + } + .onFailure { dex2oatArray[dex2oatArray.indexOf(path)] = null } + } + + private fun notMounted(): Boolean { + for (i in 0 until 4) { + val bin = dex2oatArray[i] ?: continue + try { + val apex = Os.stat("/proc/1/root$bin") + val wrapper = Os.stat(if (i < 2) WRAPPER32 else WRAPPER64) + if (apex.st_dev != wrapper.st_dev || apex.st_ino != wrapper.st_ino) { + return true + } + } catch (e: ErrnoException) { + return true + } + } + return false + } + + private fun doMount(enabled: Boolean) { + doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]) + } + + fun start() { + if (notMounted()) { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + return + } + } + + selinuxObserver.startWatching() + selinuxObserver.onEvent(0, null) + + // Run the socket accept loop in an IO coroutine + CoroutineScope(Dispatchers.IO).launch { runSocketLoop() } + } + + private fun runSocketLoop() { + Log.i(TAG, "Dex2oat wrapper daemon start") + val sockPath = getSockPath() + Log.d(TAG, "wrapper path: $sockPath") + + val xposedFile = "u:object_r:xposed_file:s0" + val dex2oatExec = "u:object_r:dex2oat_exec:s0" + + if (SELinux.checkSELinuxAccess("u:r:dex2oat:s0", dex2oatExec, "file", "execute_no_trans")) { + SELinux.setFileContext(WRAPPER32, dex2oatExec) + SELinux.setFileContext(WRAPPER64, dex2oatExec) + setSockCreateContext("u:r:dex2oat:s0") + } else { + SELinux.setFileContext(WRAPPER32, xposedFile) + SELinux.setFileContext(WRAPPER64, xposedFile) + setSockCreateContext("u:r:installd:s0") + } + SELinux.setFileContext(HOOKER32, xposedFile) + SELinux.setFileContext(HOOKER64, xposedFile) + + runCatching { + LocalServerSocket(sockPath).use { server -> + setSockCreateContext(null) + while (true) { + // This blocks until the C++ wrapper connects + server.accept().use { client -> + val input = client.inputStream + val output = client.outputStream + val id = input.read() + if (id in fdArray.indices && fdArray[id] != null) { + client.setFileDescriptorsForSend(arrayOf(fdArray[id]!!)) + output.write(1) + } + } + } + } + } + .onFailure { + Log.e(TAG, "Dex2oat wrapper daemon crashed", it) + if (compatibility == DEX2OAT_OK) { + doMount(false) + compatibility = DEX2OAT_CRASHED + } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt new file mode 100644 index 000000000..060c62202 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -0,0 +1,186 @@ +package org.matrix.vector.daemon.env + +import android.annotation.SuppressLint +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.SELinux +import android.os.SystemProperties +import android.system.Os +import android.util.Log +import java.io.File +import java.io.FileDescriptor +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Paths +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.data.FileSystem + +private const val TAG = "VectorLogcat" +private const val FD_MODE = + ParcelFileDescriptor.MODE_WRITE_ONLY or + ParcelFileDescriptor.MODE_CREATE or + ParcelFileDescriptor.MODE_TRUNCATE or + ParcelFileDescriptor.MODE_APPEND + +object LogcatMonitor { + private var modulesFd = -1 + private var verboseFd = -1 + @Volatile private var isRunning = false + + private external fun runLogcat() + + // Thread-safe LRU implementation for log files + private class ThreadSafeLRU(private val maxEntries: Int = 10) { + private val map = LinkedHashMap(maxEntries, 1f, false) + + @Synchronized + fun add(file: File) { + map[file] = Unit + if (map.size > maxEntries) { + val eldest = map.keys.first() + if (eldest.delete()) { + map.remove(eldest) + } + } + } + } + + private val moduleLogs = ThreadSafeLRU() + private val verboseLogs = ThreadSafeLRU() + + init { + loadNativeLibrary() + FileSystem.moveLogDir() // Defined in FileSystem + + // Meizu log_reject_level workaround + if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) { + SystemProperties.set("persist.sys.log_reject_level", "0") + } + + dumpPropsAndDmesg() + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + private fun loadNativeLibrary() { + val classPath = System.getProperty("java.class.path", "") + val abi = + if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS[0] else Build.SUPPORTED_32_BIT_ABIS[0] + System.load("$classPath!/lib/$abi/${System.mapLibraryName("daemon")}") + } + + private fun dumpPropsAndDmesg() { + CoroutineScope(Dispatchers.IO).launch { + // Filter privacy props by temporarily assuming an untrusted context + runCatching { + SELinux.setFSCreateContext("u:object_r:app_data_file:s0") + ProcessBuilder( + "sh", + "-c", + "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop") + .redirectOutput(FileSystem.getPropsPath()) // Ensure this exists in FileSystem + .start() + } + .onFailure { Log.e(TAG, "getprop failed", it) } + .also { SELinux.setFSCreateContext(null) } + + runCatching { ProcessBuilder("dmesg").redirectOutput(FileSystem.getKmsgPath()).start() } + .onFailure { Log.e(TAG, "dmesg failed", it) } + } + } + + fun start() { + if (isRunning) return + isRunning = true + CoroutineScope(Dispatchers.IO).launch { + runCatching { + Log.i(TAG, "Logcat daemon starting") + runLogcat() // Blocks until the native logcat process dies + Log.i(TAG, "Logcat daemon stopped") + } + .onFailure { Log.e(TAG, "Logcat crashed", it) } + isRunning = false + } + } + + fun getVerboseLog(): File? = fdToPath(verboseFd)?.toFile() + + fun getModulesLog(): File? = fdToPath(modulesFd)?.toFile() + + private fun fdToPath(fd: Int) = if (fd == -1) null else Paths.get("/proc/self/fd", fd.toString()) + + /** Resurrects deleted log files from /proc/self/fd if an external process deletes them. */ + private fun checkFd(fd: Int) { + if (fd == -1) return + runCatching { + val jfd = FileDescriptor() + jfd.javaClass + .getDeclaredMethod("setInt\$", Int::class.java) + .apply { isAccessible = true } + .invoke(jfd, fd) + val stat = Os.fstat(jfd) + + // st_nlink == 0 means the file was deleted but the FD is still held open + if (stat.st_nlink == 0L) { + val file = Files.readSymbolicLink(fdToPath(fd)!!) + val parent = file.parent + if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { + if (FileSystem.chattr0(parent)) Files.deleteIfExists(parent) + } + val name = file.fileName.toString() + val originName = name.substring(0, name.lastIndexOf(' ')) + Files.copy(file, parent.resolve(originName)) + } + } + .onFailure { Log.w(TAG, "checkFd failed for $fd", it) } + } + + fun startVerbose() = Log.i(TAG, "!!start_verbose!!") + + fun stopVerbose() = Log.i(TAG, "!!stop_verbose!!") + + fun enableWatchdog() = Log.i(TAG, "!!start_watchdog!!") + + fun disableWatchdog() = Log.i(TAG, "!!stop_watchdog!!") + + fun refresh(isVerboseLog: Boolean) { + Log.i(TAG, if (isVerboseLog) "!!refresh_verbose!!" else "!!refresh_modules!!") + } + + fun checkLogFile() { + if (modulesFd == -1) refresh(false) + if (verboseFd == -1) refresh(true) + } + + @Suppress("unused") // Called via JNI + private fun refreshFd(isVerboseLog: Boolean): Int { + return runCatching { + val logFile = + if (isVerboseLog) { + checkFd(verboseFd) + val f = FileSystem.getNewVerboseLogPath() + verboseLogs.add(f) + f + } else { + checkFd(modulesFd) + val f = FileSystem.getNewModulesLogPath() + moduleLogs.add(f) + f + } + + Log.i(TAG, "New log file: $logFile") + FileSystem.chattr0(logFile.toPath().parent) + val fd = ParcelFileDescriptor.open(logFile, FD_MODE).detachFd() + + if (isVerboseLog) verboseFd = fd else modulesFd = fd + fd + } + .onFailure { + if (isVerboseLog) verboseFd = -1 else modulesFd = -1 + Log.w(TAG, "refreshFd failed", it) + } + .getOrDefault(-1) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt new file mode 100644 index 000000000..1016da143 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -0,0 +1,133 @@ +package org.matrix.vector.daemon.ipc + +import android.os.IBinder +import android.os.Parcel +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.RemoteException +import android.util.Log +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.service.ILSPApplicationService +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.utils.InstallerVerifier +import org.matrix.vector.daemon.utils.ObfuscationManager + +private const val TAG = "VectorAppService" + +// Hardcoded transaction code from BridgeService +const val BRIDGE_TRANSACTION_CODE = + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code +const val DEX_TRANSACTION_CODE = + ('_'.code shl 24) or ('D'.code shl 16) or ('E'.code shl 8) or 'X'.code +const val OBFUSCATION_MAP_TRANSACTION_CODE = + ('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code + +object ApplicationService : ILSPApplicationService.Stub() { + + data class ProcessKey(val uid: Int, val pid: Int) + + private val processes = ConcurrentHashMap() + + private class ProcessInfo(val key: ProcessKey, val processName: String, val heartBeat: IBinder) : + IBinder.DeathRecipient { + init { + heartBeat.linkToDeath(this, 0) + processes[key] = this + } + + override fun binderDied() { + heartBeat.unlinkToDeath(this, 0) + processes.remove(key) + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + when (code) { + DEX_TRANSACTION_CODE -> { + val shm = FileSystem.getPreloadDex(PreferenceStore.isDexObfuscateEnabled()) ?: return false + reply?.writeNoException() + reply?.let { shm.writeToParcel(it, 0) } + reply?.writeLong(shm.size.toLong()) + return true + } + OBFUSCATION_MAP_TRANSACTION_CODE -> { + val obfuscation = PreferenceStore.isDexObfuscateEnabled() + val signatures = ObfuscationManager.getSignatures() + reply?.writeNoException() + reply?.writeInt(signatures.size * 2) + for ((key, value) in signatures) { + reply?.writeString(key) + reply?.writeString(if (obfuscation) value else key) + } + return true + } + } + return super.onTransact(code, data, reply, flags) + } + + fun registerHeartBeat(uid: Int, pid: Int, processName: String, heartBeat: IBinder): Boolean { + return runCatching { + ProcessInfo(ProcessKey(uid, pid), processName, heartBeat) + true + } + .getOrDefault(false) + } + + fun hasRegister(uid: Int, pid: Int): Boolean = processes.containsKey(ProcessKey(uid, pid)) + + private fun ensureRegistered(): ProcessInfo { + val key = ProcessKey(getCallingUid(), getCallingPid()) + val info = processes[key] + if (info == null) { + Log.w(TAG, "Unauthorized IPC call from uid=${key.uid} pid=${key.pid}") + throw RemoteException("Not registered") + } + return info + } + + private fun getAllModules(): List { + val info = ensureRegistered() + if (info.key.uid == Process.SYSTEM_UID && info.processName == "system") { + return ConfigCache.getModulesForSystemServer() + } + if (ManagerService.isRunningManager(getCallingPid(), info.key.uid)) { + return emptyList() + } + return ConfigCache.getModulesForProcess(info.processName, info.key.uid) + } + + override fun getModulesList() = getAllModules().filter { !it.file.legacy } + + override fun getLegacyModulesList() = getAllModules().filter { it.file.legacy } + + override fun isLogMuted(): Boolean = !ManagerService.isVerboseLog + + override fun getPrefsPath(packageName: String): String { + val info = ensureRegistered() + return ConfigCache.getPrefsPath(packageName, info.key.uid) + } + + override fun requestInjectedManagerBinder( + binderList: MutableList + ): ParcelFileDescriptor? { + val info = ensureRegistered() + val pid = info.key.pid + val uid = info.key.uid + + if (ManagerService.postStartManager(pid, uid) || ConfigCache.isManager(uid)) { + binderList.add(ManagerService.obtainManagerBinder(info.heartBeat, pid, uid)) + } + + return runCatching { + // Verify the APK signature before serving it + InstallerVerifier.verifyInstallerSignature(FileSystem.managerApkPath.toString()) + ParcelFileDescriptor.open( + FileSystem.managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + } + .onFailure { Log.e(TAG, "Failed to open or verify manager APK", it) } + .getOrNull() + } +} 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 new file mode 100644 index 000000000..380c7a3c5 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt @@ -0,0 +1,211 @@ +package org.matrix.vector.daemon.ipc + +import org.lsposed.lspd.models.Application +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.CliRequest +import org.matrix.vector.daemon.CliResponse +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.system.* + +object CliHandler { + + /** + * Executes the requested CLI command within the daemon's memory space. Returns a structured + * CliResponse. + */ + fun execute(request: CliRequest): CliResponse { + return try { + val responseData = + when (request.command) { + "status" -> handleStatus() + "modules" -> handleModules(request) + "scope" -> handleScope(request) + "config" -> handleConfig(request) + "log" -> handleLog(request) + else -> throw IllegalArgumentException("Unknown command: ${request.command}") + } + CliResponse(success = true, data = responseData) + } catch (e: Exception) { + CliResponse(success = false, error = e.message ?: "Unknown error occurred") + } + } + + private fun handleStatus(): Map { + return mapOf( + "Framework Version" to BuildConfig.VERSION_NAME, + "Version Code" to BuildConfig.VERSION_CODE, + "Enabled Modules" to ConfigCache.state.modules.size, + "Daemon Status" to "Running", + "Status Notification" to PreferenceStore.isStatusNotificationEnabled()) + } + + private fun isPackageInstalled(pkg: String, userId: Int = 0): Boolean { + return runCatching { packageManager?.getPackageInfo(pkg, 0, userId) != null } + .getOrDefault(false) + } + + private fun handleModules(request: CliRequest): Any { + return when (request.action) { + "ls" -> { + val enabledOnly = request.options["enabled"] as? Boolean ?: false + val disabledOnly = request.options["disabled"] as? Boolean ?: false + + // Get the current immutable snapshot of enabled modules + val enabledModuleKeys = ConfigCache.state.modules.keys + // Get all installed modules from the system + val installed = ConfigCache.getInstalledModules() + + // Map to the CLI view model + installed + .mapNotNull { info -> + val pkg = info.packageName + val isEnabled = enabledModuleKeys.contains(pkg) + + // Filter based on CLI flags + if (enabledOnly && !isEnabled) return@mapNotNull null + if (disabledOnly && isEnabled) return@mapNotNull null + + mapOf( + "PACKAGE" to pkg, + "UID" to info.uid, + "STATUS" to (if (isEnabled) "enabled" else "disabled")) + } + .sortedBy { it["PACKAGE"] as String } + } + "enable" -> { + if (request.targets.isEmpty()) + throw IllegalArgumentException("No packages provided to enable.") + val success = mutableListOf() + val failed = mutableListOf() + request.targets.forEach { pkg -> + if (ModuleDatabase.enableModule(pkg)) success.add(pkg) else failed.add(pkg) + } + mapOf("Enabled" to success, "Failed" to failed) + } + "disable" -> { + if (request.targets.isEmpty()) + throw IllegalArgumentException("No packages provided to disable.") + val success = mutableListOf() + val failed = mutableListOf() + request.targets.forEach { pkg -> + if (ModuleDatabase.disableModule(pkg)) success.add(pkg) else failed.add(pkg) + } + mapOf("Disabled" to success, "Failed" to failed) + } + else -> throw IllegalArgumentException("Unknown module action: ${request.action}") + } + } + + private fun handleScope(request: CliRequest): Any { + if (request.targets.isEmpty()) throw IllegalArgumentException("Module package name required.") + val modulePkg = request.targets[0] + val apps = request.targets.drop(1) + + return when (request.action) { + "ls" -> { + val scope = + ConfigCache.getModuleScope(modulePkg) + ?: throw IllegalArgumentException("Module not found: $modulePkg") + scope.map { mapOf("APP_PACKAGE" to it.packageName, "USER_ID" to it.userId) } + } + "add" -> { + if (apps.isEmpty()) throw IllegalArgumentException("No target apps provided.") + val scope = ConfigCache.getModuleScope(modulePkg) ?: mutableListOf() + + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + if (scope.none { it.packageName == pkg && it.userId == user }) { + scope.add( + Application().apply { + packageName = pkg + userId = user + }) + } + } + ModuleDatabase.setModuleScope(modulePkg, scope) + "Successfully appended ${apps.size} apps to $modulePkg scope." + } + "set" -> { + if (apps.isEmpty()) + throw IllegalArgumentException("No target apps provided for scope overwrite.") + 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 { + packageName = pkg + userId = user + }) + } + ModuleDatabase.setModuleScope(modulePkg, scope) + "Successfully overwrote scope for $modulePkg (${apps.size} apps)." + } + "rm" -> { + if (apps.isEmpty()) throw IllegalArgumentException("No target apps provided to remove.") + var removedCount = 0 + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + if (ModuleDatabase.removeModuleScope(modulePkg, pkg, user)) removedCount++ + } + "Successfully removed $removedCount apps from $modulePkg scope." + } + else -> throw IllegalArgumentException("Unknown scope action: ${request.action}") + } + } + + private fun handleConfig(request: CliRequest): Any { + val keys = request.targets + return when (request.action) { + "get" -> { + if (keys.isEmpty()) throw IllegalArgumentException("Config key required.") + val key = keys[0] + val value = + when (key) { + "dex-obfuscate" -> ManagerService.dexObfuscate + "status-notification" -> ManagerService.enableStatusNotification() + "log-watchdog" -> ManagerService.isLogWatchdogEnabled + "verbose-log" -> ManagerService.isVerboseLog + else -> throw IllegalArgumentException("Unknown config key: $key") + } + mapOf("KEY" to key, "VALUE" to value) + } + "set" -> { + if (keys.size < 2) throw IllegalArgumentException("Key and value required.") + val key = keys[0] + val value = + keys[1].toBooleanStrictOrNull() + ?: throw IllegalArgumentException("Value must be 'true' or 'false'.") + + when (key) { + "dex-obfuscate" -> ManagerService.dexObfuscate = value + "status-notification" -> ManagerService.setEnableStatusNotification(value) + "log-watchdog" -> ManagerService.setLogWatchdog(value) + "verbose-log" -> ManagerService.setVerboseLog(value) + else -> throw IllegalArgumentException("Unknown config key: $key") + } + "Successfully set $key to $value." + } + else -> throw IllegalArgumentException("Unknown config action: ${request.action}") + } + } + + private fun handleLog(request: CliRequest): Any { + return when (request.action) { + "clear" -> { + val verbose = request.options["verbose"] as? Boolean ?: false + ManagerService.clearLogs(verbose) + "Logs cleared successfully." + } + // "stream" is handled in SystemServerService.kt to attach the FileDescriptor + else -> throw IllegalArgumentException("Unknown log action: ${request.action}") + } + } +} 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 new file mode 100644 index 000000000..392dca2a7 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt @@ -0,0 +1,76 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Binder +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.util.Log +import io.github.libxposed.service.IXposedService +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.FileSystem +import org.matrix.vector.daemon.data.PreferenceStore +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 + private val callbacks = ConcurrentHashMap>() + + override fun getFrameworkProperties(): Long { + var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE + if (PreferenceStore.isDexObfuscateEnabled()) { + prop = prop or IXposedService.PROP_RT_API_PROTECTION + } + return prop + } + + override fun requestRemotePreferences( + group: String, + callback: IRemotePreferenceCallback? + ): Bundle { + val bundle = Bundle() + val userId = Binder.getCallingUid() / PER_USER_RANGE + bundle.putSerializable( + "map", PreferenceStore.getModulePrefs(packageName, userId, group) as Serializable) + + if (callback != null) { + val groupCallbacks = callbacks.getOrPut(group) { ConcurrentHashMap.newKeySet() } + groupCallbacks.add(callback) + runCatching { callback.asBinder().linkToDeath({ groupCallbacks.remove(callback) }, 0) } + .onFailure { Log.w(TAG, "requestRemotePreferences linkToDeath failed", it) } + } + return bundle + } + + 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) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun getRemoteFileList(): Array { + val userId = Binder.getCallingUid() / PER_USER_RANGE + return runCatching { + val dir = FileSystem.resolveModuleDir(packageName, "files", userId, -1) + dir.toFile().list() ?: emptyArray() + } + .getOrElse { throw RemoteException(it.message) } + } + + // Called by ModuleService when prefs are updated globally + fun onUpdateRemotePreferences(group: String, diff: Bundle) { + val groupCallbacks = callbacks[group] ?: return + for (callback in groupCallbacks) { + runCatching { callback.onUpdate(diff) }.onFailure { groupCallbacks.remove(callback) } + } + } +} 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 new file mode 100644 index 000000000..a90df46ea --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -0,0 +1,487 @@ +package org.matrix.vector.daemon.ipc + +import android.content.ComponentName +import android.content.Context +import android.content.IIntentSender +import android.content.Intent +import android.content.IntentSender +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.VersionedPackage +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.os.ParcelFileDescriptor +import android.os.SELinux +import android.os.SystemProperties +import android.util.Log +import android.view.IWindowManager +import hidden.HiddenApiBridge +import io.github.libxposed.service.IXposedService +import java.io.File +import java.io.FileOutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.lsposed.lspd.ILSPManagerService +import org.lsposed.lspd.models.Application +import org.lsposed.lspd.models.UserInfo +import org.matrix.vector.daemon.BuildConfig +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.env.Dex2OatServer +import org.matrix.vector.daemon.env.LogcatMonitor +import org.matrix.vector.daemon.system.* +import org.matrix.vector.daemon.utils.applyXspaceWorkaround +import org.matrix.vector.daemon.utils.getRealUsers +import rikka.parcelablelist.ParcelableListSlice + +private const val TAG = "VectorManagerService" + +object ManagerService : ILSPManagerService.Stub() { + + @Volatile private var managerPid = -1 + @Volatile private var pendingManager = false + @Volatile private var isEnabled = true + + private var managerIntent: Intent? = null + + var guard: ManagerGuard? = null + internal set + + class ManagerGuard(private val binder: IBinder, val pid: Int, val uid: Int) : + IBinder.DeathRecipient { + private val connection = + object : android.app.IServiceConnection.Stub() { + override fun connected(name: ComponentName?, service: IBinder?, dead: Boolean) {} + } + + init { + ManagerService.guard = this + runCatching { + binder.linkToDeath(this, 0) + applyXspaceWorkaround(connection) + } + .onFailure { + Log.e(TAG, "ManagerGuard initialization failed", it) + ManagerService.guard = null + } + } + + override fun binderDied() { + runCatching { + binder.unlinkToDeath(this, 0) + activityManager?.unbindService(connection) + } + ManagerService.guard = null + } + } + + @Synchronized + fun preStartManager(): Boolean { + pendingManager = true + managerPid = -1 + return true + } + + @Synchronized + fun shouldStartManager(pid: Int, uid: Int, processName: String): Boolean { + if (!isEnabled || + uid != BuildConfig.MANAGER_INJECTED_UID || + processName != BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME || + !pendingManager) + return false + pendingManager = false + managerPid = pid + return true + } + + private fun getManagerIntent(): Intent? { + if (managerIntent != null) return managerIntent + runCatching { + var intent = + Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_INFO) + setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + } + var ris = packageManager?.queryIntentActivitiesCompat(intent, intent.type, 0, 0) + + if (ris.isNullOrEmpty()) { + intent.removeCategory(Intent.CATEGORY_INFO) + intent.addCategory(Intent.CATEGORY_LAUNCHER) + ris = packageManager?.queryIntentActivitiesCompat(intent, intent.type, 0, 0) + } + + if (ris.isNullOrEmpty()) { + val pkgInfo = + packageManager?.getPackageInfoCompat( + BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0) + val activity = pkgInfo?.activities?.firstOrNull { it.processName == it.packageName } + if (activity != null) { + intent = + Intent(Intent.ACTION_MAIN).apply { + component = ComponentName(activity.packageName, activity.name) + } + } else return null + } else { + val activity = ris.first().activityInfo + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.component = ComponentName(activity.packageName, activity.name) + } + + intent.categories?.clear() + intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER") + intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + managerIntent = Intent(intent) + } + .onFailure { Log.e(TAG, "Failed to build manager intent", it) } + return managerIntent + } + + fun openManager(withData: Uri?) { + val intent = getManagerIntent() ?: return + val launchIntent = Intent(intent).apply { data = withData } + runCatching { + activityManager?.startActivityAsUserWithFeature( + SystemContext.appThread, + "android", + null, + launchIntent, + launchIntent.type, + null, + null, + 0, + 0, + null, + null, + 0) + } + .onFailure { Log.e(TAG, "Failed to open manager", it) } + } + + fun postStartManager(pid: Int, uid: Int): Boolean = + isEnabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid + + /** Fixes permissions for the WebView cache. */ + private fun fixWebViewPermissions(file: File, targetUid: Int) { + if (!file.exists()) return + + // Set the SELinux label that allows apps to read/write shared xposed data + SELinux.setFileContext(file.absolutePath, "u:object_r:xposed_file:s0") + + // Change ownership to the target UID (e.g., 2000) + runCatching { android.system.Os.chown(file.absolutePath, targetUid, targetUid) } + .onFailure { Log.e(TAG, "Failed to chown ${file.path}", it) } + + // Recurse into directories + if (file.isDirectory) { + file.listFiles()?.forEach { fixWebViewPermissions(it, targetUid) } + } + } + + private fun ensureWebViewPermission() { + val targetUid = BuildConfig.MANAGER_INJECTED_UID + runCatching { + val pkgInfo = + packageManager?.getPackageInfoCompat(BuildConfig.MANAGER_INJECTED_PKG_NAME, 0, 0) + ?: return@runCatching + + val dataDir = + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(pkgInfo.applicationInfo) + val cacheDir = File(dataDir, "cache") + + if (!cacheDir.exists()) cacheDir.mkdirs() + fixWebViewPermissions(cacheDir, targetUid) + } + .onFailure { Log.w(TAG, "WebView permission fix failed", it) } + } + + fun obtainManagerBinder(heartbeat: IBinder, pid: Int, uid: Int): IBinder { + ManagerGuard(heartbeat, pid, uid) + if (uid == BuildConfig.MANAGER_INJECTED_UID) { + ensureWebViewPermission() + } + return this + } + + fun isRunningManager(pid: Int, uid: Int): Boolean = false + + override fun getXposedApiVersion() = IXposedService.LIB_API + + override fun getXposedVersionCode() = BuildConfig.VERSION_CODE + + override fun getXposedVersionName() = BuildConfig.VERSION_NAME + + override fun getInstalledPackagesFromAllUsers( + flags: Int, + filterNoProcess: Boolean + ): ParcelableListSlice { + return ParcelableListSlice( + packageManager?.getInstalledPackagesFromAllUsers(flags, filterNoProcess) ?: emptyList()) + } + + override fun enabledModules() = ConfigCache.state.modules.keys.toTypedArray() + + override fun enableModule(packageName: String) = ModuleDatabase.enableModule(packageName) + + override fun disableModule(packageName: String) = ModuleDatabase.disableModule(packageName) + + override fun setModuleScope(packageName: String, scope: MutableList) = + ModuleDatabase.setModuleScope(packageName, scope) + + override fun getModuleScope(packageName: String) = ConfigCache.getModuleScope(packageName) + + override fun isVerboseLog() = PreferenceStore.isVerboseLogEnabled() || BuildConfig.DEBUG + + override fun setVerboseLog(enabled: Boolean) { + if (isVerboseLog()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() + PreferenceStore.setVerboseLog(enabled) + } + + 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) + } + } + + override fun clearLogs(verbose: Boolean): 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) + } + + override fun reboot() { + powerManager?.reboot(false, null, false) + } + + override fun uninstallPackage(packageName: String, userId: Int): Boolean { + val latch = CountDownLatch(1) + var result = false + + val sender = + object : IIntentSender.Stub() { + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: android.content.IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + val status = + intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + result = status == PackageInstaller.STATUS_SUCCESS + latch.countDown() + } + + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + finishedReceiver: android.content.IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ): Int { + send(code, intent, resolvedType, null, finishedReceiver, requiredPermission, options) + return 0 + } + } + + // Using reflection to wrap the AIDL stub into an Android IntentSender + val intentSender = + runCatching { + val constructor = + IntentSender::class.java.getDeclaredConstructor(IIntentSender::class.java) + constructor.isAccessible = true + constructor.newInstance(sender) + } + .getOrNull() ?: return false + + val pkg = VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST) + val flag = if (userId == -1) 0x00000002 else 0 // DELETE_ALL_USERS flag + + runCatching { + packageManager + ?.packageInstaller + ?.uninstall(pkg, "android", flag, intentSender, if (userId == -1) 0 else userId) + } + .onFailure { + return false + } + + latch.await() + return result + } + + override fun isSepolicyLoaded() = + android.os.SELinux.checkSELinuxAccess( + "u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans") + + override fun getUsers(): List { + return userManager?.getRealUsers()?.map { + UserInfo().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 startActivityAsUserWithFeature(intent: Intent, userId: Int): Int { + if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { + intent.removeExtra("lsp_no_switch_to_user") + val currentUser = activityManager?.currentUser + val parent = userManager?.getProfileParent(userId)?.id ?: userId + if (currentUser != null && currentUser.id != parent) { + if (activityManager?.switchUser(parent) == false) return -1 + val wm = + IWindowManager.Stub.asInterface( + android.os.ServiceManager.getService(Context.WINDOW_SERVICE)) + wm?.lockNow(null) + } + } + return activityManager?.startActivityAsUserWithFeature( + SystemContext.appThread, + "android", + null, + intent, + intent.type, + null, + null, + 0, + 0, + null, + null, + userId) ?: -1 + } + + override fun queryIntentActivitiesAsUser( + intent: Intent, + flags: Int, + userId: Int + ): ParcelableListSlice { + return ParcelableListSlice( + packageManager?.queryIntentActivitiesCompat(intent, intent.type, flags, userId) + ?: emptyList()) + } + + override fun dex2oatFlagsLoaded() = + SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0") + + override fun setHiddenIcon(hide: Boolean) { + val args = + Bundle().apply { + putString("value", if (hide) "0" else "1") + putString("_user", "0") + } + runCatching { + val provider = + activityManager + ?.getContentProviderExternal("settings", 0, SystemContext.token, null) + ?.provider + provider?.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args) + } + .onFailure { Log.w(TAG, "setHiddenIcon failed", it) } + } + + override fun getLogs(zipFd: ParcelFileDescriptor) { + FileSystem.getLogs(zipFd) + } + + override fun restartFor(intent: Intent) {} // No-op matching original + + override fun getDenyListPackages() = ConfigCache.getDenyListPackages() + + /** + * Executes Magisk via ProcessBuilder and redirects output directly to the passed + * ParcelFileDescriptor using the /proc/self/fd/ pseudo-filesystem. + */ + override fun flashZip(zipPath: String, outputStream: ParcelFileDescriptor) { + val fdFile = File("/proc/self/fd/${outputStream.fd}") + val processBuilder = + ProcessBuilder("magisk", "--install-module", zipPath) + .redirectOutput(ProcessBuilder.Redirect.appendTo(fdFile)) + + runCatching { + outputStream.use { _ -> + FileOutputStream(fdFile, true).use { fdw -> + val proc = processBuilder.start() + if (proc.waitFor(10, TimeUnit.SECONDS)) { + if (proc.exitValue() == 0) { + fdw.write("- Reboot after 5s\n".toByteArray()) + Thread.sleep(5000) + reboot() + } else { + fdw.write("! Flash failed, exit with ${proc.exitValue()}\n".toByteArray()) + } + } else { + proc.destroy() + fdw.write("! Timeout, abort\n".toByteArray()) + } + } + } + } + .onFailure { Log.e(TAG, "flashZip failed", it) } + } + + override fun clearApplicationProfileData(packageName: String) { + packageManager?.clearApplicationProfileData(packageName) + } + + override fun enableStatusNotification() = PreferenceStore.isStatusNotificationEnabled() + + override fun setEnableStatusNotification(enable: Boolean) { + val isEnabled = enableStatusNotification() + PreferenceStore.setStatusNotification(enable) + if (isEnabled && !enable) { + NotificationManager.cancelStatusNotification() + } + if (!isEnabled && enable) { + NotificationManager.notifyStatusNotification() + } + } + + override fun performDexOptMode(packageName: String) = + org.matrix.vector.daemon.utils.performDexOptMode(packageName) + + override fun getDexObfuscate() = PreferenceStore.isDexObfuscateEnabled() + + override fun setDexObfuscate(enabled: Boolean) = PreferenceStore.setDexObfuscate(enabled) + + override fun getDex2OatWrapperCompatibility() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dex2OatServer.compatibility else 0 + + override fun setLogWatchdog(enabled: Boolean) = PreferenceStore.setLogWatchdog(enabled) + + override fun isLogWatchdogEnabled() = PreferenceStore.isLogWatchdogEnabled() + + override fun setAutoInclude(packageName: String, enabled: Boolean) = + ModuleDatabase.setAutoInclude(packageName, enabled) + + override fun getAutoInclude(packageName: String) = ConfigCache.getAutoInclude(packageName) +} 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 new file mode 100644 index 000000000..aca2cacb8 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -0,0 +1,213 @@ +package org.matrix.vector.daemon.ipc + +import android.content.AttributionSource +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.util.Log +import io.github.libxposed.service.IXposedScopeCallback +import io.github.libxposed.service.IXposedService +import java.io.Serializable +import java.util.Collections +import java.util.WeakHashMap +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.BuildConfig +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 +import org.matrix.vector.daemon.system.activityManager + +private const val TAG = "VectorModuleService" + +class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { + + companion object { + private val uidSet = ConcurrentHashMap.newKeySet() + private val serviceMap = Collections.synchronizedMap(WeakHashMap()) + + fun uidClear() { + uidSet.clear() + } + + fun uidStarts(uid: Int) { + if (uidSet.add(uid)) { + val module = ConfigCache.getModuleByUid(uid) + if (module?.file?.legacy == false) { + val service = serviceMap.getOrPut(module) { ModuleService(module) } + service.sendBinder(uid) + } + } + } + + fun uidGone(uid: Int) { + uidSet.remove(uid) + } + } + + /** + * Forges a ContentProvider call to force the module's target app process to receive this Binder + * IPC endpoint without standard Context.bindService() limits. + */ + private fun sendBinder(uid: Int) { + val name = loadedModule.packageName + runCatching { + val userId = uid / PER_USER_RANGE + val authority = name + AUTHORITY_SUFFIX + val provider = + activityManager?.getContentProviderExternal(authority, userId, null, null)?.provider + + if (provider == null) { + Log.d(TAG, "No service provider for $name") + return + } + + val extra = Bundle().apply { putBinder("binder", asBinder()) } + val reply: Bundle? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + provider.call( + AttributionSource.Builder(1000).setPackageName("android").build(), + authority, + SEND_BINDER, + null, + extra) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + provider.call("android", null, authority, SEND_BINDER, null, extra) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + provider.call("android", authority, SEND_BINDER, null, extra) + } else { + provider.call("android", SEND_BINDER, null, extra) + } + + if (reply != null) Log.d(TAG, "Sent module binder to $name") + else Log.w(TAG, "Failed to send module binder to $name") + } + .onFailure { Log.w(TAG, "Failed to send module binder for uid $uid", it) } + } + + private fun ensureModule(): Int { + val appId = Binder.getCallingUid() % PER_USER_RANGE + if (loadedModule.appId != appId) { + throw RemoteException( + "Module ${loadedModule.packageName} is not for uid ${Binder.getCallingUid()}") + } + return Binder.getCallingUid() / PER_USER_RANGE + } + + override fun getApiVersion() = ensureModule().let { IXposedService.LIB_API } + + override fun getFrameworkName() = ensureModule().let { BuildConfig.FRAMEWORK_NAME } + + override fun getFrameworkVersion() = ensureModule().let { BuildConfig.VERSION_NAME } + + override fun getFrameworkVersionCode() = ensureModule().let { BuildConfig.VERSION_CODE } + + override fun getFrameworkProperties(): Long { + ensureModule() + var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE + if (PreferenceStore.isDexObfuscateEnabled()) + prop = prop or IXposedService.PROP_RT_API_PROTECTION + return prop + } + + override fun getScope(): List { + ensureModule() + return ConfigCache.getModuleScope(loadedModule.packageName)?.map { it.packageName } + ?: emptyList() + } + + override fun requestScope(packages: List, callback: IXposedScopeCallback) { + val userId = ensureModule() + if (!PreferenceStore.isScopeRequestBlocked(loadedModule.packageName)) { + packages.forEach { pkg -> + NotificationManager.requestModuleScope(loadedModule.packageName, userId, pkg, callback) + } + } else { + callback.onScopeRequestFailed("Scope request blocked by user configuration") + } + } + + override fun removeScope(packages: List) { + val userId = ensureModule() + packages.forEach { pkg -> + runCatching { ModuleDatabase.removeModuleScope(loadedModule.packageName, pkg, userId) } + .onFailure { Log.e(TAG, "Error removing scope for $pkg", it) } + } + } + + override fun requestRemotePreferences(group: String): Bundle { + val userId = ensureModule() + return Bundle().apply { + putSerializable( + "map", + PreferenceStore.getModulePrefs(loadedModule.packageName, userId, group) as Serializable) + } + } + + @Suppress("DEPRECATION") + override fun updateRemotePreferences(group: String, diff: Bundle) { + val userId = ensureModule() + val values = mutableMapOf() + + diff.getSerializable("delete")?.let { deletes -> + (deletes as Set<*>).forEach { values[it as String] = null } + } + diff.getSerializable("put")?.let { puts -> + (puts as Map<*, *>).forEach { (k, v) -> values[k as String] = v } + } + + runCatching { + PreferenceStore.updateModulePrefs(loadedModule.packageName, userId, group, values) + (loadedModule.service as? InjectedModuleService)?.onUpdateRemotePreferences(group, diff) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemotePreferences(group: String) { + PreferenceStore.deleteModulePrefs(loadedModule.packageName, ensureModule(), group) + } + + override fun listRemoteFiles(): Array { + val userId = ensureModule() + return runCatching { + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .toFile() + .list() ?: emptyArray() + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun openRemoteFile(path: String): ParcelFileDescriptor { + val userId = ensureModule() + FileSystem.ensureModuleFilePath(path) + return runCatching { + val file = + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .resolve(path) + .toFile() + ParcelFileDescriptor.open( + file, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemoteFile(path: String): Boolean { + val userId = ensureModule() + FileSystem.ensureModuleFilePath(path) + return runCatching { + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .resolve(path) + .toFile() + .delete() + } + .getOrElse { throw RemoteException(it.message) } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt new file mode 100644 index 000000000..74c94a353 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt @@ -0,0 +1,121 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Build +import android.os.IBinder +import android.os.IServiceCallback +import android.os.Parcel +import android.os.ServiceManager +import android.os.SystemProperties +import android.util.Log +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.service.ILSPSystemServerService +import org.matrix.vector.daemon.* +import org.matrix.vector.daemon.system.getSystemServiceManager + +private const val TAG = "VectorSystemServer" + +class SystemServerService(private val maxRetry: Int, private val proxyServiceName: String) : + ILSPSystemServerService.Stub(), IBinder.DeathRecipient { + + private var originService: IBinder? = null + private var requestedRetryCount = -maxRetry + + companion object { + var systemServerRequested = false + } + + init { + Log.d(TAG, "registering via proxy $proxyServiceName") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val callback = + object : IServiceCallback.Stub() { + override fun onRegistration(name: String, binder: IBinder?) { + if (name == proxyServiceName && + binder != null && + binder !== this@SystemServerService) { + Log.d(TAG, "Intercepted system service registration: $name") + originService = binder + runCatching { binder.linkToDeath(this@SystemServerService, 0) } + } + } + + override fun asBinder(): IBinder = this + } + runCatching { getSystemServiceManager().registerForNotifications(proxyServiceName, callback) } + .onFailure { Log.e(TAG, "Failed to register IServiceCallback", it) } + } + } + + fun putBinderForSystemServer() { + ServiceManager.addService(proxyServiceName, this) + binderDied() + } + + override fun requestApplicationService( + uid: Int, + pid: Int, + processName: String, + heartBeat: IBinder? + ): ILSPApplicationService? { + if (uid != 1000 || heartBeat == null || processName != "system") return null + systemServerRequested = true + + // Return the ApplicationService singleton if successfully registered + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { + ApplicationService + } else null + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + originService?.let { + // This should however never happen, as service registration enforces later replacements + Log.i(TAG, "Original service $proxyServiceName alive, transmitting requests") + return it.transact(code, data, reply, flags) + } + + when (code) { + BRIDGE_TRANSACTION_CODE -> { + val uid = data.readInt() + val pid = data.readInt() + val processName = data.readString() ?: "" + val heartBeat = data.readStrongBinder() + + val service = requestApplicationService(uid, pid, processName, heartBeat) + if (service != null) { + reply?.writeNoException() + reply?.writeStrongBinder(service.asBinder()) + return true + } + return false + } + DEX_TRANSACTION_CODE, + OBFUSCATION_MAP_TRANSACTION_CODE -> { + return ApplicationService.onTransact(code, data, reply, flags) + } + else -> { + return super.onTransact(code, data, reply, flags) + } + } + } + + override fun binderDied() { + originService?.unlinkToDeath(this, 0) + originService = null + } + + fun maybeRetryInject() { + if (requestedRetryCount < 0) { + Log.w(TAG, "System server injection fails, triggering restart...") + requestedRetryCount++ + val restartTarget = + if (Build.SUPPORTED_64_BIT_ABIS.isNotEmpty() && + Build.SUPPORTED_32_BIT_ABIS.isNotEmpty()) { + "zygote_secondary" + } else { + "zygote" + } + SystemProperties.set("ctl.restart", restartTarget) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt new file mode 100644 index 000000000..3bf25000a --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt @@ -0,0 +1,269 @@ +package org.matrix.vector.daemon.system + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Icon +import android.graphics.drawable.LayerDrawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import io.github.libxposed.service.IXposedScopeCallback +import java.util.UUID +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.R +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.utils.FakeContext + +private const val TAG = "VectorNotifManager" +private const val STATUS_CHANNEL_ID = "vector_status" +private const val UPDATED_CHANNEL_ID = "vector_module_updated" +private const val STATUS_NOTIF_ID = 2000 + +object NotificationManager { + val openManagerAction = UUID.randomUUID().toString() + val moduleScopeAction = UUID.randomUUID().toString() + + val SCOPE_CHANNEL_ID = "vector_module_scope" + + private val nm: android.app.INotificationManager? by + SystemService( + Context.NOTIFICATION_SERVICE, android.app.INotificationManager.Stub::asInterface) + private val opPkg = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) "android" else "com.android.settings" + + private fun createChannels() { + val context = FakeContext() + val list = + listOf( + NotificationChannel( + STATUS_CHANNEL_ID, + context.getString(R.string.status_channel_name), + android.app.NotificationManager.IMPORTANCE_MIN) + .apply { setShowBadge(false) }, + NotificationChannel( + UPDATED_CHANNEL_ID, + context.getString(R.string.module_updated_channel_name), + android.app.NotificationManager.IMPORTANCE_HIGH) + .apply { setShowBadge(false) }, + NotificationChannel( + SCOPE_CHANNEL_ID, + context.getString(R.string.scope_channel_name), + android.app.NotificationManager.IMPORTANCE_HIGH) + .apply { setShowBadge(false) }) + runCatching { + nm?.createNotificationChannelsForPackage( + "android", 1000, android.content.pm.ParceledListSlice(list)) + } + .onFailure { Log.e(TAG, "Failed to create notification channels", it) } + } + + private fun getBitmap(id: Int): Bitmap { + val r = FileSystem.resources + var res = r.getDrawable(id, r.newTheme()) + if (res is BitmapDrawable) { + return res.bitmap + } else { + if (res is AdaptiveIconDrawable) { + res = LayerDrawable(arrayOf(res.background, res.foreground)) + } + val bitmap = + Bitmap.createBitmap(res.intrinsicWidth, res.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + res.setBounds(0, 0, canvas.width, canvas.height) + res.draw(canvas) + return bitmap + } + } + + private fun getNotificationIcon(): Icon { + return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)) + } + + fun notifyStatusNotification() { + val context = FakeContext() + val intent = Intent(openManagerAction).apply { setPackage("android") } + val pi = + PendingIntent.getBroadcast( + context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val notif = + Notification.Builder(context, STATUS_CHANNEL_ID) + .setContentTitle(context.getString(R.string.vector_running_notification_title)) + .setContentText(context.getString(R.string.vector_running_notification_content)) + .setSmallIcon(getNotificationIcon()) + .setContentIntent(pi) + .setVisibility(Notification.VISIBILITY_SECRET) + .setOngoing(true) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, null, STATUS_NOTIF_ID, notif, 0) + } + } + + fun cancelStatusNotification() { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", null, STATUS_NOTIF_ID, 0) + } else { + nm?.cancelNotificationWithTag("android", null, STATUS_NOTIF_ID, 0) + } + } + } + + fun cancelNotification(channel: String, modulePkg: String, moduleUserId: Int) { + runCatching { + // We use the module package name's hash code as the notification ID + // to match how we enqueued it in requestModuleScope and notifyModuleUpdated. + val notifId = modulePkg.hashCode() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", modulePkg, notifId, 0) + } else { + nm?.cancelNotificationWithTag("android", modulePkg, notifId, 0) + } + } + .onFailure { Log.e(TAG, "Failed to cancel notification", it) } + } + + fun requestModuleScope( + modulePkg: String, + moduleUserId: Int, + scopePkg: String, + callback: IXposedScopeCallback + ) { + val context = FakeContext() + val userName = userManager?.getUserName(moduleUserId) ?: moduleUserId.toString() + + fun createActionIntent(actionParams: String, requestCode: Int): PendingIntent { + val intent = + Intent(moduleScopeAction).apply { + setPackage("android") + data = + Uri.Builder() + .scheme("module") + .encodedAuthority("$modulePkg:$moduleUserId") + .encodedPath(scopePkg) + .appendQueryParameter("action", actionParams) + .build() + putExtras(Bundle().apply { putBinder("callback", callback.asBinder()) }) + } + return PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + val notif = + Notification.Builder(context, SCOPE_CHANNEL_ID) + .setContentTitle(context.getString(R.string.xposed_module_request_scope_title)) + .setContentText( + context.getString( + R.string.xposed_module_request_scope_content, modulePkg, userName, scopePkg)) + .setSmallIcon(getNotificationIcon()) + .addAction( + Notification.Action.Builder( + null, + context.getString(R.string.scope_approve), + createActionIntent("approve", 4)) + .build()) + .addAction( + Notification.Action.Builder( + null, context.getString(R.string.scope_deny), createActionIntent("deny", 5)) + .build()) + .addAction( + Notification.Action.Builder( + null, + context.getString(R.string.never_ask_again), + createActionIntent("block", 6)) + .build()) + .setAutoCancel(true) + .setStyle( + Notification.BigTextStyle() + .bigText( + context.getString( + R.string.xposed_module_request_scope_content, + modulePkg, + userName, + scopePkg))) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, modulePkg, modulePkg.hashCode(), notif, 0) + } + } + + fun notifyModuleUpdated( + modulePackageName: String, + moduleUserId: Int, + enabled: Boolean, + systemModule: Boolean + ) { + val context = FakeContext() + val userName = userManager?.getUserName(moduleUserId) ?: moduleUserId.toString() + + val title = + context.getString( + if (enabled) { + if (systemModule) R.string.xposed_module_updated_notification_title_system + else R.string.xposed_module_updated_notification_title + } else R.string.module_is_not_activated_yet) + + val content = + context.getString( + if (enabled) { + if (systemModule) R.string.xposed_module_updated_notification_content_system + else R.string.xposed_module_updated_notification_content + } else { + if (moduleUserId == 0) R.string.module_is_not_activated_yet_main_user_detailed + else R.string.module_is_not_activated_yet_multi_user_detailed + }, + modulePackageName, + userName) + + val intent = + Intent(openManagerAction).apply { + setPackage("android") + data = + Uri.Builder() + .scheme("module") + .encodedAuthority("$modulePackageName:$moduleUserId") + .build() + } + val pi = + PendingIntent.getBroadcast( + context, 3, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val notif = + Notification.Builder(context, UPDATED_CHANNEL_ID) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(getNotificationIcon()) + .setContentIntent(pi) + .setVisibility(Notification.VISIBILITY_SECRET) + .setAutoCancel(true) + .setStyle(Notification.BigTextStyle().bigText(content)) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag( + "android", opPkg, modulePackageName, modulePackageName.hashCode(), notif, 0) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt new file mode 100644 index 000000000..dd2a66439 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt @@ -0,0 +1,67 @@ +package org.matrix.vector.daemon.system + +import android.app.IActivityManager +import android.content.Context +import android.content.pm.IPackageManager +import android.os.IBinder +import android.os.IPowerManager +import android.os.IServiceManager +import android.os.IUserManager +import android.os.RemoteException +import com.android.internal.os.BinderInternal +import hidden.HiddenApiBridge +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * A thread-safe, lazy property delegate that fetches an Android system service Binder. + * Automatically links a DeathRecipient to clear the cache if the service dies. + */ +class SystemService(private val name: String, private val asInterface: (IBinder) -> T) : + ReadOnlyProperty { + @Volatile private var instance: T? = null + + private val deathRecipient = IBinder.DeathRecipient { instance = null } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + instance?.let { + return it + } + return synchronized(this) { + instance?.let { + return it + } + val binder = android.os.ServiceManager.getService(name) ?: return null + try { + binder.linkToDeath(deathRecipient, 0) + instance = asInterface(binder) + instance + } catch (e: RemoteException) { + null + } + } + } +} + +// --- Top-level System Binders --- +val activityManager: IActivityManager? by + SystemService(Context.ACTIVITY_SERVICE, IActivityManager.Stub::asInterface) +val packageManager: IPackageManager? by SystemService("package", IPackageManager.Stub::asInterface) +val userManager: IUserManager? by + SystemService(Context.USER_SERVICE, IUserManager.Stub::asInterface) +val powerManager: IPowerManager? by + SystemService(Context.POWER_SERVICE, IPowerManager.Stub::asInterface) + +/** + * Holds global state received from system_server during the late injection phase. Used for forging + * calls to ActivityManager that require a valid caller context. + */ +object SystemContext { + @Volatile var appThread: android.app.IApplicationThread? = null + @Volatile var token: IBinder? = null +} + +fun getSystemServiceManager(): IServiceManager { + return IServiceManager.Stub.asInterface( + HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject())) +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt new file mode 100644 index 000000000..971025c38 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -0,0 +1,328 @@ +package org.matrix.vector.daemon.system + +import android.app.IActivityManager +import android.app.IUidObserver +import android.content.IIntentReceiver +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.IPackageManager +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.ParceledListSlice +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IUserManager +import android.util.Log +import java.io.File +import java.lang.reflect.Method +import java.util.stream.Collectors +import org.matrix.vector.daemon.utils.getRealUsers + +private const val TAG = "VectorSystem" +const val PER_USER_RANGE = 100000 +const val MATCH_ANY_USER = 0x00400000 // PackageManager.MATCH_ANY_USER +const val MATCH_ALL_FLAGS = + PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.MATCH_DIRECT_BOOT_AWARE or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE or + PackageManager.MATCH_UNINSTALLED_PACKAGES or + MATCH_ANY_USER + +/** + * Internal helper that throws exceptions instead of swallowing them. This is crucial for detecting + * TransactionTooLargeException (Binder limits). + */ +@Throws(Exception::class) +private fun IPackageManager.getPackageInfoCompatThrows( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, flags.toLong(), userId) + } else { + getPackageInfo(packageName, flags, userId) + } +} + +/** Safely fetches PackageInfo, handling API level differences. */ +fun IPackageManager.getPackageInfoCompat( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + return try { + getPackageInfoCompatThrows(packageName, flags, userId) + } catch (e: Exception) { + null + } +} + +/** + * Checks if the package is truly available for the given user. Apps can be "installed" but + * disabled/hidden by profile owners. + */ +fun IPackageManager.isPackageAvailable( + packageName: String, + userId: Int, + ignoreHidden: Boolean +): Boolean { + return runCatching { + isPackageAvailable(packageName, userId) || + (ignoreHidden && getApplicationHiddenSettingAsUser(packageName, userId)) + } + .getOrDefault(false) +} + +/** Fetches PackageInfo alongside its components (Activities, Services, Receivers, Providers). */ +fun IPackageManager.getPackageInfoWithComponents( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + val fullFlags = + flags or + PackageManager.GET_ACTIVITIES or + PackageManager.GET_SERVICES or + PackageManager.GET_RECEIVERS or + PackageManager.GET_PROVIDERS + + var pkgInfo: PackageInfo? = null + + try { + // If the binder buffer overflows, it will throw an exception here. + pkgInfo = getPackageInfoCompatThrows(packageName, fullFlags, userId) + } catch (e: Exception) { + // Fallback path: Fetch sequentially if the initial query threw an Exception + pkgInfo = + try { + getPackageInfoCompatThrows(packageName, flags, userId) + } catch (ignored: Exception) { + null + } + + if (pkgInfo != null) { + runCatching { + pkgInfo.activities = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_ACTIVITIES, userId) + ?.activities + } + runCatching { + pkgInfo.services = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_SERVICES, userId) + ?.services + } + runCatching { + pkgInfo.receivers = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_RECEIVERS, userId) + ?.receivers + } + runCatching { + pkgInfo.providers = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_PROVIDERS, userId) + ?.providers + } + } + } + + if (pkgInfo?.applicationInfo == null) return null + if (pkgInfo.packageName != "android") { + val sourceDir = pkgInfo.applicationInfo?.sourceDir + if (sourceDir == null || + !File(sourceDir).exists() || + !isPackageAvailable(packageName, userId, true)) { + return null + } + } + + return pkgInfo +} + +/** Extracts all unique process names associated with a package's components. */ +fun PackageInfo.fetchProcesses(): Set { + val processNames = mutableSetOf() + + val componentArrays = arrayOf(activities, receivers, providers) + for (components in componentArrays) { + components?.forEach { processNames.add(it.processName) } + } + + services?.forEach { service -> + // Ignore isolated processes as they shouldn't be hooked in the same way + if ((service.flags and ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) { + processNames.add(service.processName) + } + } + + return processNames +} + +fun IPackageManager.queryIntentActivitiesCompat( + intent: Intent, + resolvedType: String?, + flags: Int, + userId: Int +): List { + return runCatching { + val slice = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + queryIntentActivities(intent, resolvedType, flags.toLong(), userId) + } else { + queryIntentActivities(intent, resolvedType, flags, userId) + } + slice?.list ?: emptyList() + } + .getOrElse { + Log.e(TAG, "queryIntentActivitiesCompat failed", it) + emptyList() + } +} + +fun IPackageManager.clearApplicationProfileDataCompat(packageName: String) { + runCatching { clearApplicationProfileData(packageName) } +} + +/** Cached method reference to avoid repeated reflection lookups in loops. */ +private val getInstalledPackagesMethod: Method? by lazy { + val isLongFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + android.content.pm.IPackageManager::class + .java + .declaredMethods + .find { + it.name == "getInstalledPackages" && + it.parameterTypes.size == 2 && + it.parameterTypes[0] == + (if (isLongFlags) Long::class.javaPrimitiveType else Int::class.javaPrimitiveType) + } + ?.apply { isAccessible = true } +} + +/** Reflectively calls getInstalledPackages and casts to ParceledListSlice. */ +private fun IPackageManager.getInstalledPackagesReflect( + flags: Any, + userId: Int +): List { + val method = getInstalledPackagesMethod ?: return emptyList() + return runCatching { + val result = method.invoke(this, flags, userId) + @Suppress("UNCHECKED_CAST") (result as? ParceledListSlice)?.list + } + .getOrNull() ?: emptyList() +} + +fun IPackageManager.getInstalledPackagesFromAllUsers( + flags: Int, + filterNoProcess: Boolean +): List { + val result = mutableListOf() + // Assuming userManager is available in this scope as in original code + val users = userManager?.getRealUsers() ?: emptyList() + + for (user in users) { + // We pass flags as Any so the reflective invoke handles Long or Int correctly + val flagParam: Any = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) flags.toLong() else flags + + val infos = getInstalledPackagesReflect(flagParam, user.id) + if (infos.isEmpty()) continue + + val validUserApps = + infos + .parallelStream() + .filter { + it.applicationInfo != null && (it.applicationInfo!!.uid / PER_USER_RANGE) == user.id + } + .filter { isPackageAvailable(it.packageName, user.id, true) } + .collect(Collectors.toList()) + + result.addAll(validUserApps) + } + + if (filterNoProcess) { + return result + .parallelStream() + .filter { + getPackageInfoWithComponents( + it.packageName, MATCH_ALL_FLAGS, it.applicationInfo!!.uid / PER_USER_RANGE) + ?.fetchProcesses() + ?.isNotEmpty() == true + } + .collect(Collectors.toList()) + } + + return result +} + +fun IActivityManager.registerReceiverCompat( + receiver: IIntentReceiver, + filter: IntentFilter, + requiredPermission: String?, + userId: Int, + flags: Int +): Intent? { + val appThread = SystemContext.appThread ?: return null + return runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + registerReceiverWithFeature( + appThread, + "android", + null, + "null", + receiver, + filter, + requiredPermission, + userId, + flags) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + registerReceiverWithFeature( + appThread, "android", null, receiver, filter, requiredPermission, userId, flags) + } else { + registerReceiver( + appThread, "android", receiver, filter, requiredPermission, userId, flags) + } + } + .onFailure { Log.e(TAG, "registerReceiver failed", it) } + .getOrNull() +} + +fun IActivityManager.registerUidObserverCompat(observer: IUidObserver, which: Int, cutpoint: Int) { + runCatching { registerUidObserver(observer, which, cutpoint, "android") } + .onFailure { Log.e(TAG, "registerUidObserver failed", it) } +} + +fun IActivityManager.broadcastIntentCompat(intent: Intent) { + val appThread = SystemContext.appThread + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + broadcastIntentWithFeature( + appThread, + null, + intent, + null, + null, + 0, + null, + null, + null, + null, + null, + -1, + null, + true, + false, + 0) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + broadcastIntentWithFeature( + appThread, null, intent, null, null, 0, null, null, null, -1, null, true, false, 0) + } else { + broadcastIntent( + appThread, intent, null, null, 0, null, null, null, -1, null, true, false, 0) + } + } + .onFailure { Log.e(TAG, "broadcastIntent failed", it) } +} + +fun IUserManager.getUserName(userId: Int): String { + return runCatching { getUserInfo(userId)?.name }.getOrNull() ?: userId.toString() +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt new file mode 100644 index 000000000..0013d8808 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt @@ -0,0 +1,89 @@ +package org.matrix.vector.daemon.utils + +import android.content.ContentResolver +import android.content.ContextWrapper +import android.content.pm.ApplicationInfo +import android.content.res.Resources +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteDatabase.CursorFactory +import android.os.Build +import hidden.HiddenApiBridge +import java.io.File +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.system.packageManager as sysPackageManager + +/** + * A stub context used by the daemon to forge intents and notifications without triggering + * system_server strict mode violations. + */ +class FakeContext(private val fakePackageName: String = "android") : ContextWrapper(null) { + + companion object { + @Volatile var nullProvider = false + private var systemAppInfo: ApplicationInfo? = null + private var fakeTheme: Resources.Theme? = null + } + + override fun getPackageName(): String = fakePackageName + + override fun getOpPackageName(): String = "android" + + fun getUserId(): Int { + return 0 + } + + fun getUser(): android.os.UserHandle { + return HiddenApiBridge.UserHandle(0) + } + + override fun getApplicationInfo(): ApplicationInfo { + if (systemAppInfo == null) { + systemAppInfo = + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + sysPackageManager?.getApplicationInfo("android", 0L, 0) + } else { + sysPackageManager?.getApplicationInfo("android", 0, 0) + } + } + .getOrNull() + } + return systemAppInfo ?: ApplicationInfo() + } + + override fun getContentResolver(): ContentResolver? { + return if (nullProvider) null else object : ContentResolver(this) {} + } + + override fun getTheme(): Resources.Theme { + if (fakeTheme == null) fakeTheme = resources.newTheme() + return fakeTheme!! + } + + override fun getResources(): Resources = FileSystem.resources + + // Required for Android 12+ + override fun getAttributionTag(): String? = null + + override fun getDatabasePath(name: String): File { + return java.io.File(name) // We pass absolute paths, so just return it directly + } + + override fun openOrCreateDatabase( + name: String, + mode: Int, + factory: CursorFactory? + ): SQLiteDatabase { + return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory) + } + + override fun openOrCreateDatabase( + name: String, + mode: Int, + factory: CursorFactory, + errorHandler: DatabaseErrorHandler? + ): SQLiteDatabase { + return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name).path, factory, errorHandler) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt new file mode 100644 index 000000000..dc7d00596 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt @@ -0,0 +1,28 @@ +package org.matrix.vector.daemon.utils + +import com.android.apksig.ApkVerifier +import java.io.File +import java.io.IOException + +object InstallerVerifier { + + @Throws(IOException::class) + fun verifyInstallerSignature(path: String) { + val verifier = ApkVerifier.Builder(File(path)).setMinCheckedPlatformVersion(27).build() + + try { + val result = verifier.verify() + if (!result.isVerified) { + throw IOException("APK signature not verified") + } + + val mainCert = result.signerCertificates[0] + if (!mainCert.encoded.contentEquals(SignInfo.CERTIFICATE)) { + val dname = mainCert.subjectX500Principal.name + throw IOException("APK signature mismatch: $dname") + } + } catch (e: Exception) { + throw IOException(e) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt new file mode 100644 index 000000000..ca6400bd4 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt @@ -0,0 +1,9 @@ +package org.matrix.vector.daemon.utils + +import android.os.SharedMemory + +object ObfuscationManager { + @JvmStatic external fun obfuscateDex(memory: SharedMemory): SharedMemory + + @JvmStatic external fun getSignatures(): Map +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt new file mode 100644 index 000000000..d245b133e --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt @@ -0,0 +1,127 @@ +package org.matrix.vector.daemon.utils + +import android.app.IServiceConnection +import android.app.Notification +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.Build +import android.os.IUserManager +import android.util.Log +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.ClassNotFoundException +import org.matrix.vector.daemon.system.* + +private const val TAG = "VectorWorkarounds" +private val isLenovo = Build.MANUFACTURER.equals("lenovo", ignoreCase = true) +private val isXiaomi = Build.MANUFACTURER.equals("xiaomi", ignoreCase = true) + +fun IUserManager.getRealUsers(): List { + val users = + runCatching { getUsers(true, true, true) } + .recoverCatching { t -> if (t is NoSuchMethodError) getUsers(true) else throw t } + .onFailure { Log.e(TAG, "All user retrieval attempts failed", it) } + .getOrDefault(emptyList()) + .toMutableList() + + if (isLenovo) { + val existingIds = users.map { it.id }.toSet() + for (i in 900..909) { + if (i !in existingIds) { + runCatching { getUserInfo(i) } + .onFailure { Log.e(TAG, "Failed to apply Lenovo's app cloning workaround", it) } + .getOrNull() + ?.let { users.add(it) } + } + } + } + return users +} + +/** Android 16 DP1 SystemUI FeatureFlag and Notification Builder workaround. */ +fun applyNotificationWorkaround() { + if (Build.VERSION.SDK_INT == 36) { + runCatching { + val feature = Class.forName("android.app.FeatureFlagsImpl") + val field = feature.getDeclaredField("systemui_is_cached").apply { isAccessible = true } + field.set(null, true) + } + .onFailure { + if (it !is ClassNotFoundException) + Log.e(TAG, "Failed to bypass systemui_is_cached flag", it) + } + } + + runCatching { Notification.Builder(FakeContext(), "notification_workaround").build() } + .onFailure { + if (it is AbstractMethodError) { + FakeContext.nullProvider = !FakeContext.nullProvider + } else { + Log.e(TAG, "Failed to build dummy notification", it) + } + } +} + +/** + * UpsideDownCake (Android 14) requires executing a shell command for dexopt, whereas older versions + * use reflection/IPC. + */ +fun performDexOptMode(packageName: String): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return runCatching { + val process = + Runtime.getRuntime().exec("cmd package compile -m speed-profile -f $packageName") + val output = BufferedReader(InputStreamReader(process.inputStream)).use { it.readText() } + val exitCode = process.waitFor() + exitCode == 0 && output.contains("Success") + } + .onFailure { Log.e(TAG, "Failed to exectute dexopt via cmd", it) } + .getOrDefault(false) + } else { + return runCatching { + packageManager?.performDexOptMode( + packageName, + false, // useJitProfiles + "speed-profile", + true, + true, + null) == true + } + .onFailure { Log.e(TAG, "Failed to invoke IPackageManager.performDexOptMode", it) } + .getOrDefault(false) + } +} + +fun applyXspaceWorkaround(connection: IServiceConnection) { + if (isXiaomi) { + val intent = + Intent().apply { + component = + ComponentName.unflattenFromString( + "com.miui.securitycore/com.miui.xspace.service.XSpaceService") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE.toLong(), + "android", + 0) + } else { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE, + "android", + 0) + } + } +} diff --git a/daemon/src/main/res/values-af/strings.xml b/daemon/src/main/res/values-af/strings.xml index 38e9953c4..8b9d290e5 100644 --- a/daemon/src/main/res/values-af/strings.xml +++ b/daemon/src/main/res/values-af/strings.xml @@ -9,13 +9,13 @@ Xposed-module is opgedateer, stelselherlaai vereis %s is opgedateer, aangesien die omvang System Framework bevat, vereis herlaai om veranderinge toe te pas Module-opdatering voltooi - LSPosed status - LSPosed gelaai - Tik die kennisgewing om bestuurder oop te maak + Vector status + Vector gelaai + Tik die kennisgewing om bestuurder oop te maak Omvang Versoek %1$s op gebruiker %2$s versoek om %3$s by sy omvang te voeg. Omvang versoek Keur goed Ontken - Moet nooit vra nie + Moet nooit vra nie diff --git a/daemon/src/main/res/values-ar/strings.xml b/daemon/src/main/res/values-ar/strings.xml index 013f1e30a..153b6a658 100644 --- a/daemon/src/main/res/values-ar/strings.xml +++ b/daemon/src/main/res/values-ar/strings.xml @@ -9,13 +9,13 @@ تم تحديث وحدة Xposed، مطلوب إعادة تشغيل النظام %s تم تحديثه، نظراً لأن النطاق يحتوي على إطار النظام، يتطلب إعادة التشغيل لتطبيق التغييرات اكتمل تحديث الوحدة - حالة LSPosed - تم تحميل LSPosed - اضغط على الإشعار لفتح المدير + حالة Vector + تم تحميل Vector + اضغط على الإشعار لفتح المدير Scope Request %1$s على %2$s طلبات المستخدم لإضافة %3$s إلى نطاقه. طلب النطاق اوافق رفض - لا تسأل أبدا + لا تسأل أبدا diff --git a/daemon/src/main/res/values-bg/strings.xml b/daemon/src/main/res/values-bg/strings.xml index 1d931004a..af52dbf0e 100644 --- a/daemon/src/main/res/values-bg/strings.xml +++ b/daemon/src/main/res/values-bg/strings.xml @@ -10,12 +10,12 @@ %s е актуализиран, тъй като обхватът съдържа System Framework, необходимо е рестартиране, за да се приложат промените Актуализация на модула е завършена Предложен статус на LSP - LSPosed заредени - Докоснете известието, за да отворите мениджъра + Vector заредени + Докоснете известието, за да отворите мениджъра Заявка за обхват %1$s при заявки от страна на потребителя %2$s за добавяне на %3$s към неговия обхват. Искане за обхват Одобряване на Отказ - Никога не питайте + Никога не питайте diff --git a/daemon/src/main/res/values-bn/strings.xml b/daemon/src/main/res/values-bn/strings.xml index ff94cfddf..36914d24a 100644 --- a/daemon/src/main/res/values-bn/strings.xml +++ b/daemon/src/main/res/values-bn/strings.xml @@ -10,12 +10,12 @@ %s আপডেট করা হয়েছে, যেহেতু সুযোগে সিস্টেম ফ্রেমওয়ার্ক রয়েছে, পরিবর্তনগুলি প্রয়োগ করার জন্য রিবুট প্রয়োজন মডিউল আপডেট সম্পূর্ণ LSPপোজড স্ট্যাটাস - LSPosed লোড - ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন + Vector লোড + ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন সুযোগ অনুরোধ ব্যবহারকারী %2$s এর সুযোগে %3$s যোগ করার অনুরোধে %1$s। সুযোগ অনুরোধ অনুমোদন করুন অস্বীকার করুন - কখনো জিজ্ঞাসা করবেন না + কখনো জিজ্ঞাসা করবেন না diff --git a/daemon/src/main/res/values-ca/strings.xml b/daemon/src/main/res/values-ca/strings.xml index 961589d2d..b5d53bc16 100644 --- a/daemon/src/main/res/values-ca/strings.xml +++ b/daemon/src/main/res/values-ca/strings.xml @@ -9,13 +9,13 @@ Mòdul Xposed actualitzat, cal reiniciar el sistema %s s\'ha actualitzat, ja que l\'abast conté System Framework, cal reiniciar per aplicar els canvis S\'ha completat l\'actualització del mòdul - Estat LSPosed - LSPosed carregat - Toqueu la notificació per obrir el gestor + Estat Vector + Vector carregat + Toqueu la notificació per obrir el gestor Sol·licitud d\'abast %1$s a l\'usuari %2$s demana afegir %3$s al seu abast. Sol·licitud d\'abast Aprovar Negar - Mai pregunteu + Mai pregunteu diff --git a/daemon/src/main/res/values-cs/strings.xml b/daemon/src/main/res/values-cs/strings.xml index 89c6aab41..fd96e36b9 100644 --- a/daemon/src/main/res/values-cs/strings.xml +++ b/daemon/src/main/res/values-cs/strings.xml @@ -9,13 +9,13 @@ Xposed modul aktualizován, je vyžadován restart systému %s byl aktualizován, a protože se provedly změny v souvislosti se Systémovým Frameworkem, je vyžadován restart pro aplikaci změn Aktualizace modulu dokončena - Stav LSPosed - LPosed načten - Klepnutím na oznámení otevřete správce + Stav Vector + LPosed načten + Klepnutím na oznámení otevřete správce Žádost o rozsah %1$s pro uživatele %2$s požaduje přidání %3$s do jeho rozsahu. Žádost o rozsah Schválit Odmítnout - Nikdy se neptat + Nikdy se neptat diff --git a/daemon/src/main/res/values-da/strings.xml b/daemon/src/main/res/values-da/strings.xml index 2a8485100..4bd015211 100644 --- a/daemon/src/main/res/values-da/strings.xml +++ b/daemon/src/main/res/values-da/strings.xml @@ -9,13 +9,13 @@ Xposed modul opdateret, system genstart kræves %s er blevet opdateret, da anvendelsesområdet indeholder System Framework, krævede genstart for at anvende ændringer Modulopdatering afsluttet - LSPosed status - LSPosed indlæst - Tryk på meddelelsen for at åbne administratoren + Vector status + Vector indlæst + Tryk på meddelelsen for at åbne administratoren Anmodning om anvendelsesområde %1$s på anmodninger fra brugeren %2$s om at tilføje %3$s til sit anvendelsesområde. Anmodning om anvendelsesområde Godkend Afvis - Spørg aldrig + Spørg aldrig diff --git a/daemon/src/main/res/values-de/strings.xml b/daemon/src/main/res/values-de/strings.xml index df200b793..f2e35f8e8 100644 --- a/daemon/src/main/res/values-de/strings.xml +++ b/daemon/src/main/res/values-de/strings.xml @@ -9,13 +9,13 @@ Xposed-Modul aktualisiert, Systemneustart erforderlich %s wurde aktualisiert, da der Geltungsbereich System-Framework enthält, ist ein Neustart erforderlich, damit die Änderungen übernommen werden Modulaktualisierung abgeschlossen - LSPosed-Status - LSPosed geladen - Auf die Benachrichtigung tippen, um den Manager zu öffnen + Vector-Status + Vector geladen + Auf die Benachrichtigung tippen, um den Manager zu öffnen Scope-Anfrage %1$s von Benutzer %2$s fordert an, %3$s zu seinem Scope-Bereich hinzuzufügen. Scope-Anfrage Genehmigen Verweigern - Niemals fragen + Niemals fragen diff --git a/daemon/src/main/res/values-el/strings.xml b/daemon/src/main/res/values-el/strings.xml index 85797fc8f..a2e25dd6b 100644 --- a/daemon/src/main/res/values-el/strings.xml +++ b/daemon/src/main/res/values-el/strings.xml @@ -9,13 +9,13 @@ Το πρόσθετο Xposed ενημερώθηκε, απαιτείται επανεκκίνηση συστήματος %s έχει ενημερωθεί, δεδομένου ότι το πεδίο εφαρμογής περιέχει Πλαίσιο Συστήματος, απαιτείται επανεκκίνηση για να εφαρμοστούν οι αλλαγές Η ενημέρωση πρόσθετου ολοκληρώθηκε - Κατάσταση LSPosed - LSPosed φορτώθηκε - Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή + Κατάσταση Vector + Vector φορτώθηκε + Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή Αίτηση για το πεδίο εφαρμογής %1$s στις αιτήσεις του χρήστη %2$s για την προσθήκη του %3$s στο πεδίο εφαρμογής του. Αίτημα πεδίου εφαρμογής Έγκριση Άρνηση - Ποτέ μην ρωτάς + Ποτέ μην ρωτάς diff --git a/daemon/src/main/res/values-es/strings.xml b/daemon/src/main/res/values-es/strings.xml index 5a4c91961..f0e39ee78 100644 --- a/daemon/src/main/res/values-es/strings.xml +++ b/daemon/src/main/res/values-es/strings.xml @@ -10,12 +10,12 @@ %s ha sido actualizado, ya que el ámbito contiene la estructura del sistema, requiere reiniciar para aplicar cambios Módulo de actualización completo LSPosición de estado - LSPosed cargado - Toca la notificación para abrir el gestor + Vector cargado + Toca la notificación para abrir el gestor Solicitud de alcance %1$s cuando el usuario %2$s solicita añadir %3$s a su ámbito. Solicitud de alcance Aprobar Denegar - Nunca preguntes + Nunca preguntes diff --git a/daemon/src/main/res/values-et/strings.xml b/daemon/src/main/res/values-et/strings.xml index 5e3a66898..da3e617f2 100644 --- a/daemon/src/main/res/values-et/strings.xml +++ b/daemon/src/main/res/values-et/strings.xml @@ -9,13 +9,13 @@ Xposed moodul uuendatud, süsteemi taaskäivitamine vajalik %s on uuendatud, kuna reguleerimisala sisaldab System Framework, vajalik taaskäivitamine, et rakendada muudatusi Mooduli uuendamine lõpetatud - LSPosedi staatus - LSPosed laaditud - Halduri avamiseks puudutage märguannet + Vectori staatus + Vector laaditud + Halduri avamiseks puudutage märguannet Ulatuse Taotlus %1$s kasutajal %2$s taotleb %3$s lisamist oma ulatusse. Ulatuse taotlus Kinnita Keela - Ära Enam Küsi + Ära Enam Küsi diff --git a/daemon/src/main/res/values-fa/strings.xml b/daemon/src/main/res/values-fa/strings.xml index 339c0e5de..cda35c5fe 100644 --- a/daemon/src/main/res/values-fa/strings.xml +++ b/daemon/src/main/res/values-fa/strings.xml @@ -9,13 +9,13 @@ ماژول Xposed به‌روزرسانی شد، نیاز به راه‌اندازی مجدد سیستم %s به‌روزرسانی شده است؛ از آنجا که محدوده شامل چارچوب سیستم است، برای اعمال تغییرات نیاز به راه‌اندازی مجدد سیستم است به‌روزرسانی ماژول کامل شد - وضعیت LSPosed - LSPosed بارگذاری شد - برای باز کردن مدیر روی اعلان ضربه بزنید + وضعیت Vector + Vector بارگذاری شد + برای باز کردن مدیر روی اعلان ضربه بزنید درخواست محدوده %1$s روی کاربر %2$s درخواست افزودن %3$s به محدوده خود را دارد. درخواست محدوده تأیید رد - هرگز نپرس + هرگز نپرس diff --git a/daemon/src/main/res/values-fi/strings.xml b/daemon/src/main/res/values-fi/strings.xml index b591d36b3..b21473c9e 100644 --- a/daemon/src/main/res/values-fi/strings.xml +++ b/daemon/src/main/res/values-fi/strings.xml @@ -9,13 +9,13 @@ Xposed moduuli päivitetty, järjestelmän uudelleenkäynnistys vaaditaan %s on päivitetty, koska soveltamisala sisältää järjestelmän kehyksen, vaaditaan uudelleenkäynnistys muutosten käyttöön Moduulin päivitys valmis - LSPosed status - LSPosed ladattu - Avaa manager napauttamalla ilmoitusta + Vector status + Vector ladattu + Avaa manager napauttamalla ilmoitusta Soveltamisalaa koskeva pyyntö %1$s käyttäjän %2$s pyynnöistä lisätä %3$s sen toimialueeseen. Laajuuspyyntö Hyväksy Kiellä - Älä koskaan kysy + Älä koskaan kysy diff --git a/daemon/src/main/res/values-fr/strings.xml b/daemon/src/main/res/values-fr/strings.xml index 2ca96bea9..0c3564c4b 100644 --- a/daemon/src/main/res/values-fr/strings.xml +++ b/daemon/src/main/res/values-fr/strings.xml @@ -1,7 +1,7 @@ - Le module LSPosed n\’est pas encore actif + Le module Vector n\’est pas encore actif %1$s a été installé, mais n\'a pas été encore activé %1$s a été installé pour l\'utilisateur %2$s, mais n\'a pas été encore activé Module Xposed mis à jour @@ -9,13 +9,13 @@ Module Xposed mis à jour, redémarrage du système requis %s a été mis à jour, étant donné que le champ d\'application est étendu au sous système, un redémarrage est nécessaire pour appliquer les changements Mise à jour du module terminée - Statut LSPosed - LSPosed chargé - Appuyer sur la notification pour ouvrir le gestionnaire + Statut Vector + Vector chargé + Appuyer sur la notification pour ouvrir le gestionnaire Demande d\'Extension de Portée %1$s sur l\'utilisateur %2$s demande d\'ajouter %3$s à son périmètre d\'action. Demande d\'Extension de Portée Approuver Refuser - Ne jamais demander + Ne jamais demander diff --git a/daemon/src/main/res/values-hi/strings.xml b/daemon/src/main/res/values-hi/strings.xml index 5d0eae764..b18a1f757 100644 --- a/daemon/src/main/res/values-hi/strings.xml +++ b/daemon/src/main/res/values-hi/strings.xml @@ -10,12 +10,12 @@ %s को अपडेट कर दिया गया है, क्योंकि स्कोप में सिस्टम फ्रेमवर्क है, परिवर्तनों को लागू करने के लिए रीबूट की आवश्यकता है मॉड्यूल अद्यतन पूर्ण एलएसपोस्ड स्थिति - LSPosed लोड किया गया - मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें + Vector लोड किया गया + मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें गुंजाइश अनुरोध उपयोगकर्ता %2$s पर %1$s इसके दायरे में %3$s जोड़ने का अनुरोध करता है। दायरा अनुरोध मंज़ूरी देना अस्वीकार करना - कभी मत पूछो + कभी मत पूछो diff --git a/daemon/src/main/res/values-hr/strings.xml b/daemon/src/main/res/values-hr/strings.xml index fa1f789f9..963882940 100644 --- a/daemon/src/main/res/values-hr/strings.xml +++ b/daemon/src/main/res/values-hr/strings.xml @@ -9,13 +9,13 @@ Xposed modul je ažuriran, potrebno je ponovno pokretanje sustava %s je ažuriran, budući da opseg sadrži System Framework, potrebno je ponovno pokretanje za primjenu promjena Ažuriranje modula dovršeno - LSPosed status - LSPosed je učitan - Dodirnite obavijest da otvorite upravitelja + Vector status + Vector je učitan + Dodirnite obavijest da otvorite upravitelja Zahtjev za opseg %1$s na korisniku %2$s zahtijeva dodavanje %3$s svom opsegu. Zahtjev za opseg Odobriti Poreći - Nikad ne pitaj + Nikad ne pitaj diff --git a/daemon/src/main/res/values-hu/strings.xml b/daemon/src/main/res/values-hu/strings.xml index bebddf236..f834c508e 100644 --- a/daemon/src/main/res/values-hu/strings.xml +++ b/daemon/src/main/res/values-hu/strings.xml @@ -9,13 +9,13 @@ Xposed modul frissítve, rendszer újraindítás szükséges %s frissítve lett, mivel a hatókör tartalmazza a System Framework-et, a változások alkalmazásához szükséges újraindítás szükséges. A modul frissítése befejeződött - LSPosed állapot - LSPosed betöltve - Érintse meg az értesítést a menedzser megnyitásához + Vector állapot + Vector betöltve + Érintse meg az értesítést a menedzser megnyitásához Hatókör kérés %1$s a felhasználó %2$s kérésére a %3$s hozzáadására a hatóköréhez. Hatókör kérés Jóváhagyás Megtagadás - Soha ne kérdezz rá + Soha ne kérdezz rá diff --git a/daemon/src/main/res/values-in/strings.xml b/daemon/src/main/res/values-in/strings.xml index 1f178a95d..f061972db 100644 --- a/daemon/src/main/res/values-in/strings.xml +++ b/daemon/src/main/res/values-in/strings.xml @@ -9,13 +9,13 @@ Modul Xposed diperbarui, diperlukan memulai ulang sistem %s telah diperbarui, karena cakupannya berisi Kerangka Sistem, diperlukan mulai ulang untuk menerapkan perubahan Pembaruan modul selesai - Status LSPosed - LSPosed dimuat - Ketuk notifikasi untuk membuka pengelola + Status Vector + Vector dimuat + Ketuk notifikasi untuk membuka pengelola Permintaan Cakupan %1$s pada pengguna %2$s meminta untuk menambahkan %3$s ke dalam cakupannya. Permintaan cakupan Menyetujui Menolak - Jangan Pernah Bertanya + Jangan Pernah Bertanya diff --git a/daemon/src/main/res/values-it/strings.xml b/daemon/src/main/res/values-it/strings.xml index 9646afd2d..9c611e04f 100644 --- a/daemon/src/main/res/values-it/strings.xml +++ b/daemon/src/main/res/values-it/strings.xml @@ -9,13 +9,13 @@ Modulo Xposed aggiornato, è necessario il riavvio del sistema %s è stato aggiornato. Poiché è abilitato per il framework di sistema, è necessario riavviare per applicare le modifiche Aggiornamento del modulo completato - Stato LSPosed - LSPosed caricato - Tocca la notifica per aprire il manager + Stato Vector + Vector caricato + Tocca la notifica per aprire il manager Richiesta attivazione %1$s sull\'utente %2$s richiede di aggiungere %3$s alle sue attivazioni. Richiesta attivazione Approva Nega - Non chiedere mai + Non chiedere mai diff --git a/daemon/src/main/res/values-iw/strings.xml b/daemon/src/main/res/values-iw/strings.xml index c9bb62b6a..a37e77231 100644 --- a/daemon/src/main/res/values-iw/strings.xml +++ b/daemon/src/main/res/values-iw/strings.xml @@ -1,21 +1,21 @@ - מודול LSPosed עדיין לא הופעל + מודול Vector עדיין לא הופעל %1$s הותקן, אך אינו מופעל עדיין %1$s הותקן למשתמש %2$s, אך אינו מופעל עדיין - מודול LSPosed עודכן + מודול Vector עודכן %s עודכן מודול Xposed עודכן, נדרש אתחול המערכת %s עודכן, מכיוון שההיקף מכיל System Framework, נדרש אתחול כדי להחיל שינויים עדכון המודול הושלם סטטוס LSPost - LSPosed נטען - הקש על ההודעה כדי לפתוח את המנהל + Vector נטען + הקש על ההודעה כדי לפתוח את המנהל Xposed_מודול_מבקש_כותרת_תחום %1$s על משתמש %2$s מבקש להוסיף %3$s להיקף שלו. תחום_שם_ערוץ אישור_תחום לְהַכּחִישׁ - לעולם אל תשאל + לעולם אל תשאל diff --git a/daemon/src/main/res/values-ja/strings.xml b/daemon/src/main/res/values-ja/strings.xml index 3b99590ab..9a567bbfd 100644 --- a/daemon/src/main/res/values-ja/strings.xml +++ b/daemon/src/main/res/values-ja/strings.xml @@ -9,13 +9,13 @@ Xposed モジュールが更新されました。システムの再起動が必要です %s が更新されました。スコープにシステムフレームワークが含まれているため、変更を適用するには再起動が必要です モジュールの更新完了通知 - LSPosed のステータス通知 - LSPosed の読み込み完了通知 - 通知をタップしてマネージャーを開きます + Vector のステータス通知 + Vector の読み込み完了通知 + 通知をタップしてマネージャーを開きます スコープのリクエスト ユーザー %2$s の %1$s が %3$s をそのスコープに追加するようリクエストしています。 スコープのリクエスト 許可 拒否 - 再度表示しない + 再度表示しない diff --git a/daemon/src/main/res/values-ko/strings.xml b/daemon/src/main/res/values-ko/strings.xml index 82a50715d..25be4ca5a 100644 --- a/daemon/src/main/res/values-ko/strings.xml +++ b/daemon/src/main/res/values-ko/strings.xml @@ -10,12 +10,12 @@ 범위에 시스템 프레임워크가 포함되어 있으므로 %s 이 업데이트되었습니다. 변경 사항을 적용하려면 재부팅해야 합니다. 모듈 업데이트 완료 LS포즈 상태 - LSPosed 로드됨 - 알림을 탭하여 관리자 열기 + Vector 로드됨 + 알림을 탭하여 관리자 열기 범위 요청 사용자 %2$s 의 %1$s 은 해당 범위에 %3$s 을 추가하도록 요청합니다. 범위 요청 승인 거부 - 다시 묻지 않음 + 다시 묻지 않음 diff --git a/daemon/src/main/res/values-ku/strings.xml b/daemon/src/main/res/values-ku/strings.xml index d442b1ae4..aef735d9b 100644 --- a/daemon/src/main/res/values-ku/strings.xml +++ b/daemon/src/main/res/values-ku/strings.xml @@ -10,12 +10,12 @@ %s hate nûve kirin, ji ber ku çarçove Çarçoveya Pergalê dihewîne, ji bo sepandina guhertinan ji nû ve destpêkirinê hewce dike Nûvekirina modulê qediya statûya LSP - LSP hate barkirin - Daxuyaniyê bikirtînin da ku rêveberê vekin + LSP hate barkirin + Daxuyaniyê bikirtînin da ku rêveberê vekin Scope Daxwaza %1$s li ser bikarhêner %2$s daxwaz dike ku %3$s li qada xwe zêde bike. Daxwaza Scope Destûrdan Înkarkirin - Never Ask + Never Ask diff --git a/daemon/src/main/res/values-lt/strings.xml b/daemon/src/main/res/values-lt/strings.xml index cd2045bca..60a491ffd 100644 --- a/daemon/src/main/res/values-lt/strings.xml +++ b/daemon/src/main/res/values-lt/strings.xml @@ -10,12 +10,12 @@ %s buvo atnaujintas, nes srityje yra System Framework, reikalingas perkrovimas, kad būtų galima taikyti pakeitimus Modulio atnaujinimas baigtas LSPatvirtintas statusas - LSPpateiktas pakrautas - Bakstelėkite pranešimą, kad atidarytumėte tvarkytuvę + LSPpateiktas pakrautas + Bakstelėkite pranešimą, kad atidarytumėte tvarkytuvę Apimties prašymas %1$s pagal naudotojo %2$s užklausas įtraukti %3$s į jo taikymo sritį. Apimties prašymas Patvirtinti Atsisakyti - Niekada neklauskite + Niekada neklauskite diff --git a/daemon/src/main/res/values-nl/strings.xml b/daemon/src/main/res/values-nl/strings.xml index 6e68eb0d0..333730fa9 100644 --- a/daemon/src/main/res/values-nl/strings.xml +++ b/daemon/src/main/res/values-nl/strings.xml @@ -1,21 +1,21 @@ - LSPosed module is nog niet geactiveerd + Vector module is nog niet geactiveerd %1$s is geïnstalleerd, maar nog niet geactiveerd %1$s is geïnstalleerd bij gebruiker %2$s, maar is nog niet geactiveerd - LSPosed module bijgewerkt + Vector module bijgewerkt %1$s is geupdate Xposed-module bijgewerkt, systeem opnieuw opstarten vereist %s is bijgewerkt, omdat het bereik een systeemkader bevat, moet je opnieuw opstarten om wijzigingen toe te passen Module update voltooid - LSPosed status - LSPosed geladen - Tik op de melding om manager te openen + Vector status + Vector geladen + Tik op de melding om manager te openen Scope verzoek %1$s op gebruiker %2$s verzoeken om %3$s toe te voegen aan het toepassingsgebied. Scope verzoek Goedkeuren Weiger - Nooit vragen + Nooit vragen diff --git a/daemon/src/main/res/values-no/strings.xml b/daemon/src/main/res/values-no/strings.xml index d275ef53a..c30428507 100644 --- a/daemon/src/main/res/values-no/strings.xml +++ b/daemon/src/main/res/values-no/strings.xml @@ -10,12 +10,12 @@ %s er blitt oppdatert, siden omfanget inneholder systemramme, nødvendig for omstart av endringene Moduloppdatering fullført LSPosert status - LSPosert lastet - Trykk på varselet for å åpne administrator + LSPosert lastet + Trykk på varselet for å åpne administrator Forespørsel om omfang %1$s på bruker %2$s ber om å legge til %3$s i omfanget. Forespørsel om omfang Vedta Benekte - Spør aldri + Spør aldri diff --git a/daemon/src/main/res/values-pl/strings.xml b/daemon/src/main/res/values-pl/strings.xml index a63aa91d8..07c40cec7 100644 --- a/daemon/src/main/res/values-pl/strings.xml +++ b/daemon/src/main/res/values-pl/strings.xml @@ -9,13 +9,13 @@ Zaktualizowano moduł Xposed, wymagane ponowne uruchomienie systemu %s został zaktualizowany, ponieważ zakres zawiera System Framework, wymagany restart aby zastosować zmiany Aktualizowanie modułu zakończone - Status LSPosed - LSPosed załadowany - Kliknij powiadomienie, by włączyć menadżer + Status Vector + Vector załadowany + Kliknij powiadomienie, by włączyć menadżer Żądanie Zakresu %1$s w użytkowniku %2$s żąda dodania %3$s do jego zakresu. Żądanie zakresu Zatwierdź Odrzuć - Nigdy nie pytaj + Nigdy nie pytaj diff --git a/daemon/src/main/res/values-pt-rBR/strings.xml b/daemon/src/main/res/values-pt-rBR/strings.xml index ff5357dc2..844219a7a 100644 --- a/daemon/src/main/res/values-pt-rBR/strings.xml +++ b/daemon/src/main/res/values-pt-rBR/strings.xml @@ -9,13 +9,13 @@ Módulo atualizado. É necessário reiniciar o sistema. %s foi atualizado. Como o escopo contém o Framework do Sistema, é necessário reiniciar para aplicar as mudanças. Atualização do módulo concluída - Status do LSPosed - LSPosed carregado - Toque na notificação para abrir o gerenciador + Status do Vector + Vector carregado + Toque na notificação para abrir o gerenciador Solicitação de escopo %1$s do usuário %2$s está solicitando para adicionar %3$s no seu escopo. Solicitação de escopo Permitir Negar - Nunca perguntar + Nunca perguntar diff --git a/daemon/src/main/res/values-pt/strings.xml b/daemon/src/main/res/values-pt/strings.xml index c036f9f32..680c9213b 100644 --- a/daemon/src/main/res/values-pt/strings.xml +++ b/daemon/src/main/res/values-pt/strings.xml @@ -9,13 +9,13 @@ Módulo Atualizado. É necessário reiniciar o sistema %s foi atualizado. Como o escopo contém o Framework do Sistema, é necessário reiniciar para aplicar as mudanças Atualização do módulo concluída - Estado do LSPosed - LSPosed carregado - Toque na notificação para abrir o gerenciador + Estado do Vector + Vector carregado + Toque na notificação para abrir o gerenciador Pedido de âmbito de aplicação %1$s sobre o utilizador %2$s pede para acrescentar %3$s ao seu âmbito de aplicação. Pedido de âmbito de aplicação Aprovar Negar - Nunca Pergunte + Nunca Pergunte diff --git a/daemon/src/main/res/values-ro/strings.xml b/daemon/src/main/res/values-ro/strings.xml index 0e7151ac6..e50349466 100644 --- a/daemon/src/main/res/values-ro/strings.xml +++ b/daemon/src/main/res/values-ro/strings.xml @@ -9,13 +9,13 @@ Modulul Xposed a fost actualizat, este necesară repornirea sistemului Modulul %s a fost actualizat. Este necesară repornirea dispozitivului, deoarece Sistemul Android face parte din configurația modulului. Actualizarea modulelor este completă - Stare LSPosed - LSPosed încărcat - Apăsați pentru a deschide managerul + Stare Vector + Vector încărcat + Apăsați pentru a deschide managerul Cerere de modificare configurație Modulul %1$s, instalat pentru utilizatorul %2$s, dorește să adauge %3$s în configurația sa. Cerere de modificare configurație Aprobați Refuzați - Nu afișați din nou + Nu afișați din nou diff --git a/daemon/src/main/res/values-ru/strings.xml b/daemon/src/main/res/values-ru/strings.xml index 75691c8d8..bca097f38 100644 --- a/daemon/src/main/res/values-ru/strings.xml +++ b/daemon/src/main/res/values-ru/strings.xml @@ -9,13 +9,13 @@ Модуль Xposed обновлён, требуется перезагрузка устройства %s обновлён; ввиду того, что системный фреймворк (System Framework) в его «охвате», требуется перезагрузка для применения изменений Обновление модуля завершено - Статус LSPosed - LSPosed загружен - Нажмите уведомление, чтобы открыть LSPosed Manager + Статус Vector + Vector загружен + Нажмите уведомление, чтобы открыть Vector Manager Запрос «охвата» %1$s (пользователь %2$s): запрашивается добавление %3$s в «охват». Запрос «охвата» Принять Отклонить - Больше не спрашивать + Больше не спрашивать diff --git a/daemon/src/main/res/values-si/strings.xml b/daemon/src/main/res/values-si/strings.xml index 14af856a0..5a2bf8bbe 100644 --- a/daemon/src/main/res/values-si/strings.xml +++ b/daemon/src/main/res/values-si/strings.xml @@ -10,12 +10,12 @@ %s යාවත්කාලීන කර ඇත, විෂය පථයේ පද්ධති රාමුව අඩංගු බැවින්, වෙනස්කම් යෙදීමට නැවත පණගැන්වීම අවශ්‍ය වේ මොඩියුල යාවත්කාලීන කිරීම සම්පූර්ණයි එල්එස්පී තත්ත්වය - LSPposed පටවා ඇත - කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න + LSPposed පටවා ඇත + කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න විෂය පථය ඉල්ලීම පරිශීලක %2$s හි %1$s එහි විෂය පථයට %3$s එකතු කරන ලෙස ඉල්ලා සිටී. විෂය පථය ඉල්ලීම අනුමත කරන්න ප්රතික්ෂේප කරන්න - කවදාවත් අහන්න එපා + කවදාවත් අහන්න එපා diff --git a/daemon/src/main/res/values-sk/strings.xml b/daemon/src/main/res/values-sk/strings.xml index bc8f4db64..9e71934c0 100644 --- a/daemon/src/main/res/values-sk/strings.xml +++ b/daemon/src/main/res/values-sk/strings.xml @@ -10,12 +10,12 @@ %s bola aktualizovaná, pretože rozsah obsahuje System Framework, potrebný reštart na uplatnenie zmien Aktualizácia modulu dokončená LSPonúkaný stav - LSPosed naložené - Ťuknutím na oznámenie otvorte správcu + Vector naložené + Ťuknutím na oznámenie otvorte správcu Žiadosť o rozsah %1$s na žiadosti používateľa %2$s o pridanie stránky %3$s do jej rozsahu. Žiadosť o rozsah Schváliť Odmietnuť - Nikdy sa nepýtajte + Nikdy sa nepýtajte diff --git a/daemon/src/main/res/values-sv/strings.xml b/daemon/src/main/res/values-sv/strings.xml index 6ba7be981..e5b63bc52 100644 --- a/daemon/src/main/res/values-sv/strings.xml +++ b/daemon/src/main/res/values-sv/strings.xml @@ -9,13 +9,13 @@ Xposed modul uppdaterad, systemomstart krävs %s har uppdaterats, eftersom omfattningen innehåller Systemramverk, krävs omstart för att tillämpa ändringar Uppdatering av modulen slutförd - LSPosed status - LSPosed laddad - Tryck på meddelandet för att öppna administratören + Vector status + Vector laddad + Tryck på meddelandet för att öppna administratören Begäran om tillämpningsområde %1$s om användaren %2$s begär att %3$s ska läggas till i dess räckvidd. Begäran om tillämpningsområde Godkänna Förneka - Fråga aldrig + Fråga aldrig diff --git a/daemon/src/main/res/values-th/strings.xml b/daemon/src/main/res/values-th/strings.xml index a00562388..38ac73703 100644 --- a/daemon/src/main/res/values-th/strings.xml +++ b/daemon/src/main/res/values-th/strings.xml @@ -9,13 +9,13 @@ อัปเดตโมดูล Xposed จำเป็นต้องรีสตาร์ทเครื่อง %s ได้รับการอัปเดตแล้ว เนื่องจาก Scope มี System Framework จึงจำเป็นต้องรีสตาร์ทเครื่องเพื่อใช้การเปลี่ยนแปลง การอัปเดตโมดูลเสร็จสมบูรณ์ - สถานะ LSPosed - LSPosed โหลดแล้ว - แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ + สถานะ Vector + Vector โหลดแล้ว + แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ คำขอ Scope %1$s ผูัใช้นี้ %2$s ขอให้เพิ่ม %3$s ใน List ของ scope. คำขอ Scope. อนุมัติ ปฎิเสธ - ไม่เคยถาม + ไม่เคยถาม diff --git a/daemon/src/main/res/values-tr/strings.xml b/daemon/src/main/res/values-tr/strings.xml index ebea2a4f6..e28cbb835 100644 --- a/daemon/src/main/res/values-tr/strings.xml +++ b/daemon/src/main/res/values-tr/strings.xml @@ -9,13 +9,13 @@ Xposed modülü güncellendi, sistemin yeniden başlatılması gerekiyor %s Güncelleme kapsamı Sistem Çerçevesi içerdiğinden, değişiklikleri uygulamak için yeniden başlatma gereklidir Modül güncellemesi tamamlandı - LSPosed durumu - LSPosed yüklendi - Yöneticiyi açmak için bildirime dokunun + Vector durumu + Vector yüklendi + Yöneticiyi açmak için bildirime dokunun Kapsam Talebi Kullanıcı %2$s %1$s kapsamına %3$s eklemek ister. Kapsam talebi Onayla Reddet - Asla Sorma + Asla Sorma diff --git a/daemon/src/main/res/values-uk/strings.xml b/daemon/src/main/res/values-uk/strings.xml index e4e6037df..73daa8ed7 100644 --- a/daemon/src/main/res/values-uk/strings.xml +++ b/daemon/src/main/res/values-uk/strings.xml @@ -9,13 +9,13 @@ Модуль Xposed оновлено, потрібно перезавантаження системи %s було оновлено, оскільки область містить System Framework, необхідне перезавантаження для застосування змін Оновлення модуля завершено - Статус LSPosed - LSPosed завантажено - Натисніть на повідомлення, щоб відкрити менеджер + Статус Vector + Vector завантажено + Натисніть на повідомлення, щоб відкрити менеджер Запит на визначення обсягу робіт %1$s на запит користувача %2$s з проханням додати %3$s до своєї області видимості. Запит обсягу робіт Затвердити Відхилити - Ніколи не питай + Ніколи не питай diff --git a/daemon/src/main/res/values-ur/strings.xml b/daemon/src/main/res/values-ur/strings.xml index 485184ea0..fc9739ed4 100644 --- a/daemon/src/main/res/values-ur/strings.xml +++ b/daemon/src/main/res/values-ur/strings.xml @@ -10,12 +10,12 @@ %s کو اپ ڈیٹ کر دیا گیا ہے، چونکہ دائرہ کار میں سسٹم فریم ورک ہے، تبدیلیاں لاگو کرنے کے لیے ریبوٹ کی ضرورت ہے۔ ماڈیول اپ ڈیٹ مکمل ہو گیا۔ ایل ایس پیز کی حیثیت - ایل ایس پیز لوڈ شدہ - مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ + ایل ایس پیز لوڈ شدہ + مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ دائرہ کار کی درخواست صارف %2$s پر %1$s اپنے دائرہ کار میں %3$s شامل کرنے کی درخواست کرتا ہے۔ دائرہ کار کی درخواست منظور کرو انکار کرنا - کبھی نہ پوچھیں۔ + کبھی نہ پوچھیں۔ diff --git a/daemon/src/main/res/values-vi/strings.xml b/daemon/src/main/res/values-vi/strings.xml index d33acebe9..f8f6e96f1 100644 --- a/daemon/src/main/res/values-vi/strings.xml +++ b/daemon/src/main/res/values-vi/strings.xml @@ -10,12 +10,12 @@ %s đã được cập nhật, vì phạm vi bao gồm Framework Hệ thống, thì khởi động lại là cần thiết để áp dụng các thay đổi Tiện ích bổ sung cập nhật hoàn tất Trạng thái hoạt động - Ứng dụng đã được tải - Nhấn để mở trình quản lý + Ứng dụng đã được tải + Nhấn để mở trình quản lý Yêu cầu phạm vi %1$s khi người dùng %2$s yêu cầu thêm %3$s vào phạm vi của nó. Phạm vi yêu cầu Chấp thuận Từ chối - Không hỏi lại + Không hỏi lại diff --git a/daemon/src/main/res/values-zh-rCN/strings.xml b/daemon/src/main/res/values-zh-rCN/strings.xml index 0642027b6..ace294bba 100644 --- a/daemon/src/main/res/values-zh-rCN/strings.xml +++ b/daemon/src/main/res/values-zh-rCN/strings.xml @@ -9,13 +9,13 @@ Xposed 模块已更新,需要重新启动 %s 已更新,由于作用域包含系统框架,需重启以应用更改 模块更新完成 - LSPosed 状态 - LSPosed 已加载 - 点按通知以打开管理器 + Vector 状态 + Vector 已加载 + 点按通知以打开管理器 作用域请求 用户 %2$s 上的 %1$s 请求将 %3$s 添加至其作用域。 作用域请求 允许 拒绝 - 不再询问 + 不再询问 diff --git a/daemon/src/main/res/values-zh-rHK/strings.xml b/daemon/src/main/res/values-zh-rHK/strings.xml index 5919b74e3..02b34d381 100644 --- a/daemon/src/main/res/values-zh-rHK/strings.xml +++ b/daemon/src/main/res/values-zh-rHK/strings.xml @@ -9,13 +9,13 @@ Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用範圍包含系統架構,需要重新啟動以套用修改。 模块更新完成 - LSPosed 狀態 - LSPosed 已載入 - 輕觸通知以開啟管理員 + Vector 狀態 + Vector 已載入 + 輕觸通知以開啟管理員 作用範圍要求 用戶 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 - 永不詢問 + 永不詢問 diff --git a/daemon/src/main/res/values-zh-rTW/strings.xml b/daemon/src/main/res/values-zh-rTW/strings.xml index dd2f463be..d085f2620 100644 --- a/daemon/src/main/res/values-zh-rTW/strings.xml +++ b/daemon/src/main/res/values-zh-rTW/strings.xml @@ -9,13 +9,13 @@ Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用域包含系統框架,需要重新啟動以套用修改。 模組更新完成 - LSPosed 狀態 - LSPosed 已載入 - 輕觸通知以開啟管理員 + Vector 狀態 + Vector 已載入 + 輕觸通知以開啟管理員 作用範圍要求 使用者 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 - 永不詢問 + 永不詢問 diff --git a/daemon/src/main/res/values/strings.xml b/daemon/src/main/res/values/strings.xml index 0d195d2dd..1b68ced85 100644 --- a/daemon/src/main/res/values/strings.xml +++ b/daemon/src/main/res/values/strings.xml @@ -1,20 +1,20 @@ - Xposed module is not activated yet + Module is not activated yet %1$s has been installed, but is not activated yet %1$s has been installed to user %2$s, but is not activated yet - Xposed module updated + Module updated %s has been updated, please force stop and restart apps in its scope - Xposed module updated, system reboot required + Module updated, system reboot required %s has been updated, since the scope contains System Framework, required reboot to apply changes Module update complete - LSPosed status - LSPosed loaded - Tap the notification to open manager + Vector status + Vector loaded + Tap the notification to open manager Scope Request %1$s on user %2$s requests to add %3$s to its scope. Scope request Approve Deny - Never Ask + Never Ask diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ec38ab0f..530c2248d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ appcenter = "5.0.5" glide = "5.0.5" okhttp = "5.3.2" ktfmt = "0.25.0" +coroutines = "1.10.2" [plugins] agp-lib = { id = "com.android.library", version.ref = "agp" } @@ -15,7 +16,6 @@ nav-safeargs = { id = "androidx.navigation.safeargs", version.ref = "nav" } autoresconfig = { id = "dev.rikka.tools.autoresconfig", version = "1.2.2" } ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } materialthemebuilder = { id = "dev.rikka.tools.materialthemebuilder", version = "1.5.1" } -lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.6" } lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.4" } [libraries] @@ -55,4 +55,6 @@ material = { module = "com.google.android.material:material", version = "1.12.0" gson = { module = "com.google.code.gson:gson", version = "2.13.2" } hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version = "6.1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +picocli = { module = "info.picocli:picocli", version = "4.7.7" } diff --git a/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java b/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java index 279e89754..15578dc7c 100644 --- a/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java +++ b/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java @@ -38,13 +38,6 @@ PackageInfo getPackageInfo(String packageName, long flags, int userId) String[] getPackagesForUid(int uid) throws RemoteException; - ParceledListSlice getInstalledPackages(int flags, int userId) - throws RemoteException; - - @RequiresApi(33) - ParceledListSlice getInstalledPackages(long flags, int userId) - throws RemoteException; - ParceledListSlice getInstalledApplications(int flags, int userId) throws RemoteException; diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java index 4e37cf25b..5e85d36c6 100644 --- a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -42,7 +42,7 @@ public final class XposedBridge { /** * @hide */ - public static final String TAG = "LSPosed-Bridge"; + public static final String TAG = "VectorLegacyBridge"; /** * @deprecated Use {@link #getXposedVersion()} instead. diff --git a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl index 4dff667c3..ba8c98bb1 100644 --- a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl +++ b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl @@ -5,7 +5,7 @@ import org.lsposed.lspd.service.ILSPApplicationService; interface ILSPosedService { ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat); - oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken, String api); + oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken); boolean preStartManager(); diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index 11e2385e9..7734c22e0 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -12,8 +12,6 @@ interface ILSPManagerService { const int DEX2OAT_SELINUX_PERMISSIVE = 3; const int DEX2OAT_SEPOLICY_INCORRECT = 4; - String getApi() = 1; - ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) = 2; String[] enabledModules() = 3; diff --git a/zygisk/module/cli b/zygisk/module/cli new file mode 100644 index 000000000..f4ade4f10 --- /dev/null +++ b/zygisk/module/cli @@ -0,0 +1,27 @@ +#!/system/bin/sh + +tmpDaemonApk="/data/local/tmp/daemon.apk" + +# Safely check for debug APK and set classpath +if [ -r "$tmpDaemonApk" ]; then + java_options="-Djava.class.path=$tmpDaemonApk" +else + dex_path="" + for DEXDIR in /data/adb/modules $(magisk --path 2>/dev/null)/.magisk/modules; do + if [ -d "$DEXDIR/zygisk_vector" ]; then + dex_path="$DEXDIR/zygisk_vector" + break + fi + done + + if [ -z "$dex_path" ]; then + echo "No vector module path found" + exit 1 + fi + + dex_path="$dex_path/daemon.apk" + java_options="-Djava.class.path=$dex_path" +fi + +# Launch the cli +exec /system/bin/app_process $java_options /system/bin --nice-name=VectorCli org.matrix.vector.daemon.Cli "$@" diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh index 7a9451dc8..44707fee7 100644 --- a/zygisk/module/customize.sh +++ b/zygisk/module/customize.sh @@ -82,7 +82,7 @@ esac ui_print "- Device platform: $ARCH ($ABI32 / $ABI64)" ui_print "- Extracting root module files" -for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex daemon.apk daemon manager.apk; do +for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex cli daemon.apk daemon manager.apk; do extract "$ZIPFILE" "$file" "$MODPATH" done @@ -132,6 +132,7 @@ set_perm_recursive "$MODPATH" 0 0 0755 0644 [ -d "$MODPATH/bin" ] && set_perm_recursive "$MODPATH/bin" 0 2000 0755 0755 u:object_r:xposed_file:s0 set_perm "$MODPATH/daemon" 0 0 0744 +set_perm "$MODPATH/cli" 0 0 0744 if [ "$(grep_prop ro.maple.enable)" = "1" ]; then ui_print "- Add ro.maple.enable=0" diff --git a/zygisk/module/daemon b/zygisk/module/daemon index 514eac7c0..faca6adef 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -43,4 +43,4 @@ fi [ "$debug" = "true" ] && log -p d -t "Vector" "Starting daemon $*" # Launch the daemon -exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.lsposed.lspd.Main "$@" >/dev/null 2>&1 +exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.matrix.vector.daemon.VectorDaemon "$@" >/dev/null 2>&1 diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index 5aeb86a20..eca96d4c7 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -78,11 +78,7 @@ object BridgeService { val at = activityThread.applicationThread as android.app.IApplicationThread val atBinder = at.asBinder() val systemCtx = activityThread.systemContext - service?.dispatchSystemServerContext( - atBinder, - Context_getActivityToken(systemCtx), - "Zygisk", - ) + service?.dispatchSystemServerContext(atBinder, Context_getActivityToken(systemCtx)) } .onFailure { Log.e(TAG, "Failed to dispatch system context", it) }