From 7acc806918c65ef57e31a9a39a4feb1efc4b91ad Mon Sep 17 00:00:00 2001 From: xihan123 Date: Sun, 23 Mar 2025 22:44:23 +0800 Subject: [PATCH 1/5] Enhance installation and cleanup processes(#29) --- manager/src/main/AndroidManifest.xml | 11 +++ .../org/lsposed/lspatch/LSPApplication.kt | 1 + .../main/java/org/lsposed/lspatch/Patcher.kt | 5 +- .../lsposed/lspatch/ui/page/NewPatchScreen.kt | 89 +++++++++++++++++-- .../lspatch/ui/page/manage/AppManagePage.kt | 6 +- .../java/org/lsposed/lspatch/ui/util/Utils.kt | 48 ++++++++++ .../ui/viewmodel/manage/AppManageViewModel.kt | 14 ++- .../lsposed/lspatch/util/LSPPackageManager.kt | 6 ++ manager/src/main/res/xml/file_paths.xml | 5 ++ 9 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 manager/src/main/res/xml/file_paths.xml diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index f3d3f14dd..45ab42d39 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> + + + + + diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt index e35023e29..55ee06045 100644 --- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt @@ -18,6 +18,7 @@ class LSPApplication : Application() { lateinit var prefs: SharedPreferences lateinit var tmpApkDir: File + lateinit var targetApkFile: File val globalScope = CoroutineScope(Dispatchers.Default) diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 15e6d3544..29ea150a0 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -10,8 +10,8 @@ import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.PatchConfig import org.lsposed.patch.LSPatch import org.lsposed.patch.util.Logger +import java.io.File import java.io.IOException -import java.util.Collections.addAll object Patcher { @@ -59,6 +59,9 @@ object Patcher { ?: throw IOException("Failed to create output file") val output = lspApp.contentResolver.openOutputStream(file.uri) ?: throw IOException("Failed to open output stream") + val apkFile = File(lspApp.externalCacheDir, apk.name) + apk.copyTo(apkFile, overwrite = true) + lspApp.targetApkFile = apkFile output.use { apk.inputStream().use { input -> input.copyTo(output) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index a1dc27ef4..d628ba58f 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -48,8 +48,11 @@ import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.util.LocalSnackbarHost +import org.lsposed.lspatch.ui.util.checkIsApkFixedByLSP +import org.lsposed.lspatch.ui.util.installApk import org.lsposed.lspatch.ui.util.isScrolledToEnd import org.lsposed.lspatch.ui.util.lastItemIndex +import org.lsposed.lspatch.ui.util.uninstallApkByPackageName import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction @@ -121,7 +124,10 @@ fun NewPatchScreen( when (viewModel.patchState) { PatchState.INIT -> { LaunchedEffect(Unit) { - LSPPackageManager.cleanTmpApkDir() + LSPPackageManager.apply { + cleanTmpApkDir() + cleanExternalTmpApkDir() + } when (id) { ACTION_STORAGE -> { storageLauncher.launch(arrayOf("application/vnd.android.package-archive")) @@ -435,10 +441,10 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { val installSuccessfully = stringResource(R.string.patch_install_successfully) val installFailed = stringResource(R.string.patch_install_failed) val copyError = stringResource(R.string.copy_error) - var installing by remember { mutableStateOf(false) } - if (installing) InstallDialog(viewModel.patchApp) { status, message -> + var installing by remember { mutableStateOf(0) } + val onFinish: (Int, String?) -> Unit = { status, message -> scope.launch { - installing = false + installing = 0 if (status == PackageInstaller.STATUS_SUCCESS) { lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } navigator.navigateUp() @@ -451,6 +457,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { } } } + if (installing == 1) InstallDialog(viewModel.patchApp, onFinish) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish) Row(Modifier.padding(top = 12.dp)) { Button( modifier = Modifier.weight(1f), @@ -462,11 +469,9 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { modifier = Modifier.weight(1f), onClick = { if (!ShizukuApi.isPermissionGranted) { - scope.launch { - snackbarHost.showSnackbar(shizukuUnavailable) - } + installing = 2 } else { - installing = true + installing = 1 } }, content = { Text(stringResource(R.string.install)) } @@ -572,3 +577,71 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { ) } } + +@Composable +private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { + val scope = rememberCoroutineScope() + var uninstallFirst by remember { + mutableStateOf( + checkIsApkFixedByLSP( + lspApp, + patchApp.app.packageName + ) + ) + } + + fun doInstall() { + Log.i(TAG, "Installing app ${patchApp.app.packageName}") + installApk(lspApp, lspApp.targetApkFile) + } + + LaunchedEffect(Unit) { + if (!uninstallFirst) { + onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") + doInstall() + } + } + + if (uninstallFirst) { + AlertDialog( + onDismissRequest = { + onFinish( + LSPPackageManager.STATUS_USER_CANCELLED, + "User cancelled" + ) + }, + confirmButton = { + TextButton( + onClick = { + onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Reset") + scope.launch { + Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") + uninstallApkByPackageName(lspApp, patchApp.app.packageName) + uninstallFirst = false + } + }, + content = { Text(stringResource(android.R.string.ok)) } + ) + }, + dismissButton = { + TextButton( + onClick = { + onFinish( + LSPPackageManager.STATUS_USER_CANCELLED, + "User cancelled" + ) + }, + content = { Text(stringResource(android.R.string.cancel)) } + ) + }, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.uninstall), + textAlign = TextAlign.Center + ) + }, + text = { Text(stringResource(R.string.patch_uninstall_text)) } + ) + } +} \ No newline at end of file diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt index a062a374c..4535147ed 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt @@ -192,11 +192,7 @@ fun AppManageBody( onClick = { expanded = false scope.launch { - if (!ShizukuApi.isPermissionGranted) { - snackbarHost.showSnackbar(shizukuUnavailable) - } else { - viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second)) - } + viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second)) } } ) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt index 0a90329d0..ccd09bd2f 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt @@ -1,6 +1,13 @@ package org.lsposed.lspatch.ui.util +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.util.Log import androidx.compose.foundation.lazy.LazyListState +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import java.io.File val LazyListState.lastVisibleItemIndex get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index @@ -10,3 +17,44 @@ val LazyListState.lastItemIndex val LazyListState.isScrolledToEnd get() = lastVisibleItemIndex == lastItemIndex + +fun checkIsApkFixedByLSP(context: Context, packageName: String): Boolean { + return try { + val app = + context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + (app.metaData?.containsKey("lspatch") != true) + } catch (_: PackageManager.NameNotFoundException) { + Log.e("LSPatch", "Package not found: $packageName") + false + } catch (e: Exception) { + Log.e("LSPatch", "Unexpected error in checkIsApkFixedByLSP", e) + false + } +} + +fun installApk(context: Context, apkFile: File) { + try { + val apkUri = + FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile) + + val intent = Intent(Intent.ACTION_VIEW).apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addCategory("android.intent.category.DEFAULT") + setDataAndType(apkUri, "application/vnd.android.package-archive") + } + context.startActivity(intent) + } catch (e: Exception) { + Log.e("LSPatch", "installApk", e) + } +} + +fun uninstallApkByPackageName(context: Context, packageName: String) = try { + val intent = Intent(Intent.ACTION_DELETE).apply { + data = "package:$packageName".toUri() + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) +} catch (e: Exception) { + Log.e("LSPatch", "uninstallApkByPackageName", e) +} \ No newline at end of file diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt index 51da66e3c..1ae5cebfd 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt @@ -17,6 +17,7 @@ import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.PatchConfig +import org.lsposed.lspatch.ui.util.installApk import org.lsposed.lspatch.ui.viewstate.ProcessingState import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo @@ -86,7 +87,10 @@ class AppManageViewModel : ViewModel() { updateLoaderState = ProcessingState.Processing val result = runCatching { withContext(Dispatchers.IO) { - LSPPackageManager.cleanTmpApkDir() + LSPPackageManager.apply { + cleanTmpApkDir() + cleanExternalTmpApkDir() + } val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray()) val patchPaths = mutableListOf() val embeddedModulePaths = mutableListOf() @@ -118,8 +122,12 @@ class AppManageViewModel : ViewModel() { } } Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths)) - val (status, message) = LSPPackageManager.install() - if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) + if (!ShizukuApi.isPermissionGranted) { + installApk(lspApp, lspApp.targetApkFile) + } else { + val (status, message) = LSPPackageManager.install() + if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) + } } } updateLoaderState = ProcessingState.Done(result) diff --git a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt index 5830b7920..fd61d8ac9 100644 --- a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt +++ b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt @@ -79,6 +79,12 @@ object LSPPackageManager { } } + suspend fun cleanExternalTmpApkDir(){ + withContext(Dispatchers.IO) { + lspApp.externalCacheDir?.listFiles()?.forEach(File::delete) + } + } + suspend fun install(): Pair { Log.i(TAG, "Perform install patched apks") var status = PackageInstaller.STATUS_FAILURE diff --git a/manager/src/main/res/xml/file_paths.xml b/manager/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..b8ff724e0 --- /dev/null +++ b/manager/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From 8dba96296b62747c92c61ad68bc9d168b4d476e5 Mon Sep 17 00:00:00 2001 From: xihan123 Date: Sun, 18 May 2025 14:59:03 +0800 Subject: [PATCH 2/5] Update Gradle wrapper to version 8.14 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c..ca025c83a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From feea434ccba46dff037b71a54d8bb20ac0afb963 Mon Sep 17 00:00:00 2001 From: xihan123 Date: Sun, 18 May 2025 15:02:08 +0800 Subject: [PATCH 3/5] The implementation of the local mode does not require the background process to stay alive --- .../LSPAppComponentFactoryStub.java | 73 ++--------- .../lspatch/loader/LSPApplication.java | 47 +++++-- .../service/FixedLocalApplicationService.java | 121 ++++++++++++++++++ .../service/RemoteApplicationService.java | 2 +- .../main/java/org/lsposed/patch/LSPatch.java | 42 +++--- 5 files changed, 196 insertions(+), 89 deletions(-) create mode 100644 patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java diff --git a/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java b/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java index d251eb226..0454af1dd 100644 --- a/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java +++ b/meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java @@ -1,36 +1,30 @@ package org.lsposed.lspatch.metaloader; import android.annotation.SuppressLint; -import android.app.AppComponentFactory; import android.app.ActivityThread; -import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; -import android.os.Build; -import android.os.Process; -import android.os.ServiceManager; -import android.util.JsonReader; +import android.app.AppComponentFactory; import android.util.Log; -import org.lsposed.hiddenapibypass.HiddenApiBypass; import org.lsposed.lspatch.share.Constants; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Method; -import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.zip.ZipFile; @SuppressLint("UnsafeDynamicallyLoadedCode") public class LSPAppComponentFactoryStub extends AppComponentFactory { private static final String TAG = "LSPatch-MetaLoader"; - private static final Map archToLib = new HashMap(4); + private static final Map archToLib = Map.of( + "arm", "armeabi-v7a", + "arm64", "arm64-v8a", + "x86", "x86", + "x86_64", "x86_64" + ); public static byte[] dex; @@ -45,11 +39,6 @@ public class LSPAppComponentFactoryStub extends AppComponentFactory { private static void bootstrap() { try { - archToLib.put("arm", "armeabi-v7a"); - archToLib.put("arm64", "arm64-v8a"); - archToLib.put("x86", "x86"); - archToLib.put("x86_64", "x86_64"); - var cl = Objects.requireNonNull(LSPAppComponentFactoryStub.class.getClassLoader()); Class VMRuntime = Class.forName("dalvik.system.VMRuntime"); Method getRuntime = VMRuntime.getDeclaredMethod("getRuntime"); @@ -59,51 +48,17 @@ private static void bootstrap() { String arch = (String) vmInstructionSet.invoke(getRuntime.invoke(null)); String libName = archToLib.get(arch); - boolean useManager = false; - String soPath; - - try (var is = cl.getResourceAsStream(Constants.CONFIG_ASSET_PATH); - var reader = new JsonReader(new InputStreamReader(is))) { - reader.beginObject(); - while (reader.hasNext()) { - var name = reader.nextName(); - if (name.equals("useManager")) { - useManager = reader.nextBoolean(); - break; - } else { - reader.skipValue(); - } - } - } - - if (useManager) { - Log.i(TAG, "Bootstrap loader from manager"); - var ipm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); - ApplicationInfo manager; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - manager = (ApplicationInfo) HiddenApiBypass.invoke(IPackageManager.class, ipm, "getApplicationInfo", Constants.MANAGER_PACKAGE_NAME, 0L, Process.myUid() / 100000); - } else { - manager = ipm.getApplicationInfo(Constants.MANAGER_PACKAGE_NAME, 0, Process.myUid() / 100000); - } - try (var zip = new ZipFile(new File(manager.sourceDir)); - var is = zip.getInputStream(zip.getEntry(Constants.LOADER_DEX_ASSET_PATH)); - var os = new ByteArrayOutputStream()) { - transfer(is, os); - dex = os.toByteArray(); - } - soPath = manager.sourceDir + "!/assets/lspatch/so/" + libName + "/liblspatch.so"; - } else { - Log.i(TAG, "Bootstrap loader from embedment"); - try (var is = cl.getResourceAsStream(Constants.LOADER_DEX_ASSET_PATH); - var os = new ByteArrayOutputStream()) { - transfer(is, os); - dex = os.toByteArray(); - } - soPath = cl.getResource("assets/lspatch/so/" + libName + "/liblspatch.so").getPath().substring(5); + Log.i(TAG, "Bootstrap loader from embedment"); + try (var is = cl.getResourceAsStream(Constants.LOADER_DEX_ASSET_PATH); + var os = new ByteArrayOutputStream()) { + transfer(is, os); + dex = os.toByteArray(); } + String soPath = cl.getResource("assets/lspatch/so/" + libName + "/liblspatch.so").getPath().substring(5); System.load(soPath); } catch (Throwable e) { + Log.e(TAG, "Error when loading liblspatch.so", e); throw new ExceptionInInitializerError(e); } } diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java index ced217ff9..80b249658 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java @@ -6,20 +6,23 @@ import android.app.ActivityThread; import android.app.LoadedApk; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.Build; -import android.os.RemoteException; import android.system.Os; import android.util.Log; +import org.json.JSONArray; +import org.json.JSONObject; import org.lsposed.lspatch.loader.util.FileUtils; import org.lsposed.lspatch.loader.util.XLog; +import org.lsposed.lspatch.service.FixedLocalApplicationService; import org.lsposed.lspatch.service.LocalApplicationService; import org.lsposed.lspatch.service.RemoteApplicationService; import org.lsposed.lspd.core.Startup; +import org.lsposed.lspd.models.Module; import org.lsposed.lspd.service.ILSPApplicationService; -import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; @@ -35,6 +38,7 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -63,7 +67,7 @@ public static boolean isIsolated() { return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID; } - public static void onLoad() throws RemoteException, IOException { + public static void onLoad() throws IOException { if (isIsolated()) { XLog.d(TAG, "Skip isolated process"); return; @@ -76,11 +80,38 @@ public static void onLoad() throws RemoteException, IOException { } Log.d(TAG, "Initialize service client"); - ILSPApplicationService service; + ILSPApplicationService service = null; if (config.optBoolean("useManager")) { - service = new RemoteApplicationService(context); - } else { - service = new LocalApplicationService(context); + try { + service = new RemoteApplicationService(context); + List m = service.getLegacyModulesList(); + JSONArray modules = new JSONArray(); + for (Module module : m) { + JSONObject json = new JSONObject(); + json.put("packageName", module.packageName); + json.put("apkPath", module.apkPath); + modules.put(json); + } + Log.i(TAG, "Modules fetched from manager: " + modules.toString()); + SharedPreferences prefs = context.getSharedPreferences("lspatch", Context.MODE_PRIVATE); + prefs.edit().putString("modules", modules.toString()).apply(); + Log.i(TAG, "Modules saved to SharedPreferences"); + } catch (Exception e) { // Catch RemoteException or others during remote service interaction + Log.e(TAG, "Failed to use RemoteApplicationService, fallback to fixed local service", e); + // Fallback service is created below if service is still null + } + } + + if (service == null) { + if (config.optBoolean("useManager")) { + // If remote failed, use FixedLocalApplicationService + service = new FixedLocalApplicationService(context); + Log.i(TAG, "Using FixedLocalApplicationService as fallback."); + } else { + // If useManager was false, use LocalApplicationService + service = new LocalApplicationService(context); + Log.i(TAG, "Using LocalApplicationService."); + } } disableProfile(context); @@ -239,4 +270,4 @@ private static void switchAllClassLoader() { } } } -} +} \ No newline at end of file diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java b/patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java new file mode 100644 index 000000000..5dfd6a7ca --- /dev/null +++ b/patch-loader/src/main/java/org/lsposed/lspatch/service/FixedLocalApplicationService.java @@ -0,0 +1,121 @@ +package org.lsposed.lspatch.service; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.lsposed.lspatch.util.ModuleLoader; +import org.lsposed.lspd.models.Module; +import org.lsposed.lspd.service.ILSPApplicationService; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FixedLocalApplicationService extends ILSPApplicationService.Stub { + + private static final String TAG = "LSPatch"; + private static final String PREFS_NAME = "lspatch"; + private static final String KEY_MODULES = "modules"; + + private final List modules = new ArrayList<>(); + + public FixedLocalApplicationService(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String moduleString = prefs.getString(KEY_MODULES, "[]"); + Log.i(TAG, "Using fixed local application service. Modules data: " + moduleString); + + try { + JSONArray modulesArray = new JSONArray(moduleString); + PackageManager pm = context.getPackageManager(); + for (int i = 0; i < modulesArray.length(); i++) { + JSONObject moduleObject = modulesArray.getJSONObject(i); + String packageName = moduleObject.getString("packageName"); + String apkPath = moduleObject.getString("apkPath"); + File apkFile = new File(apkPath); + + if (!apkFile.exists()) { + Log.w(TAG, "Module APK not found at cached path: " + apkPath + " for package: " + packageName); + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + apkPath = info.sourceDir; + apkFile = new File(apkPath); + if (apkFile.exists()) { + Log.i(TAG, "Found module APK via PackageManager: " + apkPath); + } else { + Log.e(TAG, "Module APK still not found via PackageManager for: " + packageName); + continue; // Skip this module if APK is not found + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Module package not found: " + packageName, e); + continue; // Skip this module + } + } + + Module module = new Module(); + module.apkPath = apkPath; + module.packageName = packageName; + try { + module.file = ModuleLoader.loadModule(apkPath); + if (module.file != null) { + modules.add(module); + Log.i(TAG, "Successfully loaded module: " + packageName + " from " + apkPath); + } else { + Log.e(TAG, "Failed to load module file for: " + packageName); + } + } catch (Exception loadException) { + Log.e(TAG, "Error loading module file for: " + packageName + " from " + apkPath, loadException); + } + } + } catch (JSONException e) { + Log.e(TAG, "Error parsing modules JSON", e); + } catch (Exception e) { // Catch unexpected errors during initialization + Log.e(TAG, "Unexpected error initializing FixedLocalApplicationService", e); + } + } + + @Override + public boolean isLogMuted() throws RemoteException { + // Consider making this configurable if needed + return false; + } + + @Override + public List getLegacyModulesList() { + // Return an immutable list or a copy to prevent external modification + return Collections.unmodifiableList(modules); + } + + @Override + public List getModulesList() { + // Currently returns an empty list, behavior maintained + return new ArrayList<>(); + } + + @Override + public String getPrefsPath(String packageName) { + // Consider validating packageName or handling potential exceptions + return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath(); + } + + @Override + public ParcelFileDescriptor requestInjectedManagerBinder(List binder) { + // Currently returns null, behavior maintained + return null; + } + + @Override + public IBinder asBinder() { + return this; + } +} \ No newline at end of file diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java b/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java index ab3d027e9..98b38cc38 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java @@ -76,7 +76,7 @@ public void onServiceDisconnected(ComponentName name) { if (!success) throw new TimeoutException("Bind service timeout"); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InterruptedException | TimeoutException e) { - Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show(); +// Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show(); var r = new RemoteException("Failed to get manager binder"); r.initCause(e); throw r; diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index c7907a84d..a7a4f07d1 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -283,31 +283,31 @@ public void patch(File srcApkFile, File outputFile) throws PatchError, IOExcepti } if (!useManager) { - logger.i("Adding loader dex..."); - try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) { - dstZFile.add(LOADER_DEX_ASSET_PATH, is); - } catch (Throwable e) { - throw new PatchError("Error when adding assets", e); - } - - logger.i("Adding native lib..."); - // copy so and dex files into the unzipped apk - // do not put liblspatch.so into apk!lib because x86 native bridge causes crash - for (String arch : ARCHES) { - String entryName = "assets/lspatch/so/" + arch + "/liblspatch.so"; - try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) { - dstZFile.add(entryName, is, false); // no compress for so - } catch (Throwable e) { - // More exception info - throw new PatchError("Error when adding native lib", e); - } - logger.d("added " + entryName); - } - logger.i("Embedding modules..."); embedModules(dstZFile); } + logger.i("Adding loader dex..."); + try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) { + dstZFile.add(LOADER_DEX_ASSET_PATH, is); + } catch (Throwable e) { + throw new PatchError("Error when adding assets", e); + } + + logger.i("Adding native lib..."); + // copy so and dex files into the unzipped apk + // do not put liblspatch.so into apk!lib because x86 native bridge causes crash + for (String arch : ARCHES) { + String entryName = "assets/lspatch/so/" + arch + "/liblspatch.so"; + try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) { + dstZFile.add(entryName, is, false); // no compress for so + } catch (Throwable e) { + // More exception info + throw new PatchError("Error when adding native lib", e); + } + logger.d("added " + entryName); + } + // create zip link logger.d("Creating nested apk link..."); From c05e925bebc979e73c885bbd707e0b263e5358bb Mon Sep 17 00:00:00 2001 From: xihan123 Date: Mon, 4 Aug 2025 21:41:41 +0800 Subject: [PATCH 4/5] Add support for installing split APKs and implement InstallResultReceiver --- manager/src/main/AndroidManifest.xml | 8 + .../org/lsposed/lspatch/LSPApplication.kt | 2 +- .../main/java/org/lsposed/lspatch/Patcher.kt | 9 +- .../lspatch/ui/activity/MainActivity.kt | 18 +++ .../lsposed/lspatch/ui/page/NewPatchScreen.kt | 146 ++++++++++++++---- .../java/org/lsposed/lspatch/ui/util/Utils.kt | 128 +++++++++++++++ .../ui/viewmodel/manage/AppManageViewModel.kt | 12 +- 7 files changed, 286 insertions(+), 37 deletions(-) diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index 45ab42d39..daa739730 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -34,6 +34,14 @@ + + + + + + diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt index 55ee06045..c3e529138 100644 --- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt @@ -18,7 +18,7 @@ class LSPApplication : Application() { lateinit var prefs: SharedPreferences lateinit var tmpApkDir: File - lateinit var targetApkFile: File + var targetApkFiles: ArrayList? = null val globalScope = CoroutineScope(Dispatchers.Default) diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 29ea150a0..4e8f38231 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -1,5 +1,6 @@ package org.lsposed.lspatch +import android.util.Log import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.Dispatchers @@ -17,7 +18,7 @@ object Patcher { class Options( private val injectDex: Boolean, - private val config: PatchConfig, + val config: PatchConfig, private val apkPaths: List, private val embeddedModules: List? ) { @@ -52,6 +53,8 @@ object Patcher { root.listFiles().forEach { if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete() } + lspApp.targetApkFiles?.clear() + val apkFileList = arrayListOf() lspApp.tmpApkDir.walk() .filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } .forEach { apk -> @@ -61,13 +64,15 @@ object Patcher { ?: throw IOException("Failed to open output stream") val apkFile = File(lspApp.externalCacheDir, apk.name) apk.copyTo(apkFile, overwrite = true) - lspApp.targetApkFile = apkFile + apkFileList.add(apkFile) +// Log.d("lspatch", "Patched file: ${apk.name} -> ${apkFile.absolutePath}") output.use { apk.inputStream().use { input -> input.copyTo(output) } } } + lspApp.targetApkFiles = apkFileList logger.i("Patched files are saved to ${root.uri.lastPathSegment}") } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt index 017d2bb58..ed09eb520 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt @@ -1,5 +1,8 @@ package org.lsposed.lspatch.ui.activity +import android.annotation.SuppressLint +import android.content.IntentFilter +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -8,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -21,13 +25,22 @@ import org.lsposed.lspatch.ui.page.appCurrentDestinationAsState import org.lsposed.lspatch.ui.page.destinations.Destination import org.lsposed.lspatch.ui.page.startAppDestination import org.lsposed.lspatch.ui.theme.LSPTheme +import org.lsposed.lspatch.ui.util.InstallResultReceiver import org.lsposed.lspatch.ui.util.LocalSnackbarHost class MainActivity : ComponentActivity() { + private val splitInstallReceiver by lazy { InstallResultReceiver() } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") @OptIn(ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS)) + } setContent { val navController = rememberAnimatedNavController() LSPTheme { @@ -47,6 +60,11 @@ class MainActivity : ComponentActivity() { } } } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(splitInstallReceiver) + } } @Composable diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index d628ba58f..7be683944 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -50,6 +50,7 @@ import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.checkIsApkFixedByLSP import org.lsposed.lspatch.ui.util.installApk +import org.lsposed.lspatch.ui.util.installApks import org.lsposed.lspatch.ui.util.isScrolledToEnd import org.lsposed.lspatch.ui.util.lastItemIndex import org.lsposed.lspatch.ui.util.uninstallApkByPackageName @@ -78,22 +79,27 @@ fun NewPatchScreen( val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current val errorUnknown = stringResource(R.string.error_unknown) - val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> - if (apks.isEmpty()) { - navigator.navigateUp() - return@rememberLauncherForActivityResult - } - runBlocking { - LSPPackageManager.getAppInfoFromApks(apks) - .onSuccess { - viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) - } - .onFailure { - lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown) } - navigator.navigateUp() - } + val storageLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> + if (apks.isEmpty()) { + navigator.navigateUp() + return@rememberLauncherForActivityResult + } + runBlocking { + LSPPackageManager.getAppInfoFromApks(apks) + .onSuccess { + viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) + } + .onFailure { + lspApp.globalScope.launch { + snackbarHost.showSnackbar( + it.message ?: errorUnknown + ) + } + navigator.navigateUp() + } + } } - } var showSelectModuleDialog by remember { mutableStateOf(false) } val noXposedModules = stringResource(R.string.patch_no_xposed_module) @@ -158,6 +164,7 @@ fun NewPatchScreen( } } } + PatchState.SELECTING -> { resultRecipient.onNavResult { Log.d(TAG, "onNavResult: $it") @@ -170,6 +177,7 @@ fun NewPatchScreen( } } } + else -> { Scaffold( topBar = { @@ -178,6 +186,7 @@ fun NewPatchScreen( PatchState.PATCHING, PatchState.FINISHED, PatchState.ERROR -> CenterAlignedTopAppBar(title = { Text(viewModel.patchApp.app.packageName) }) + else -> Unit } }, @@ -203,10 +212,12 @@ fun NewPatchScreen( } if (showSelectModuleDialog) { - AlertDialog(onDismissRequest = { showSelectModuleDialog = false }, + AlertDialog( + onDismissRequest = { showSelectModuleDialog = false }, confirmButton = {}, dismissButton = { - TextButton(content = { Text(stringResource(android.R.string.cancel)) }, + TextButton( + content = { Text(stringResource(android.R.string.cancel)) }, onClick = { showSelectModuleDialog = false }) }, title = { @@ -218,7 +229,8 @@ fun NewPatchScreen( }, text = { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - TextButton(modifier = Modifier.fillMaxWidth(), + TextButton( + modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { storageModuleLauncher.launch(arrayOf("application/vnd.android.package-archive")) @@ -230,11 +242,13 @@ fun NewPatchScreen( style = MaterialTheme.typography.bodyLarge ) } - TextButton(modifier = Modifier.fillMaxWidth(), + TextButton( + modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { navigator.navigate( - SelectAppsScreenDestination(true, + SelectAppsScreenDestination( + true, viewModel.embeddedModules.mapTo(ArrayList()) { it.app.packageName }) ) showSelectModuleDialog = false @@ -323,7 +337,12 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { extraContent = { TextButton( onClick = onAddEmbed, - content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) } + content = { + Text( + text = stringResource(R.string.patch_embed_modules), + style = MaterialTheme.typography.bodyLarge + ) + } ) } ) @@ -337,7 +356,9 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { title = stringResource(R.string.patch_debuggable) ) SettingsCheckBox( - modifier = Modifier.clickable { viewModel.overrideVersionCode = !viewModel.overrideVersionCode }, + modifier = Modifier.clickable { + viewModel.overrideVersionCode = !viewModel.overrideVersionCode + }, checked = viewModel.overrideVersionCode, icon = Icons.Outlined.Layers, title = stringResource(R.string.patch_override_version_code), @@ -369,7 +390,9 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton(selected = viewModel.sigBypassLevel == it, onClick = { viewModel.sigBypassLevel = it }) + RadioButton( + selected = viewModel.sigBypassLevel == it, + onClick = { viewModel.sigBypassLevel = it }) Text(sigBypassLvStr(it)) } }, @@ -421,7 +444,10 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { when (it.first) { Log.DEBUG -> Text(text = it.second) Log.INFO -> Text(text = it.second) - Log.ERROR -> Text(text = it.second, color = MaterialTheme.colorScheme.error) + Log.ERROR -> Text( + text = it.second, + color = MaterialTheme.colorScheme.error + ) } } } @@ -446,18 +472,26 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { scope.launch { installing = 0 if (status == PackageInstaller.STATUS_SUCCESS) { - lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } + lspApp.globalScope.launch { + snackbarHost.showSnackbar( + installSuccessfully + ) + } navigator.navigateUp() } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) { val result = snackbarHost.showSnackbar(installFailed, copyError) if (result == SnackbarResult.ActionPerformed) { - val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val cm = + lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message)) } } } } - if (installing == 1) InstallDialog(viewModel.patchApp, onFinish) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish) + if (installing == 1) InstallDialog( + viewModel.patchApp, + onFinish + ) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish) Row(Modifier.padding(top = 12.dp)) { Button( modifier = Modifier.weight(1f), @@ -478,6 +512,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { ) } } + PatchState.ERROR -> { Row(Modifier.padding(top = 12.dp)) { Button( @@ -489,13 +524,19 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { Button( modifier = Modifier.weight(1f), onClick = { - val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip(ClipData.newPlainText("LSPatch", viewModel.logs.joinToString { it.second + "\n" })) + val cm = + lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip( + ClipData.newPlainText( + "LSPatch", + viewModel.logs.joinToString { it.second + "\n" }) + ) }, content = { Text(stringResource(R.string.copy_error)) } ) } } + else -> Unit } } @@ -505,7 +546,13 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { @Composable private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { val scope = rememberCoroutineScope() - var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) } + var uninstallFirst by remember { + mutableStateOf( + ShizukuApi.isPackageInstalledWithoutPatch( + patchApp.app.packageName + ) + ) + } var installing by remember { mutableStateOf(0) } suspend fun doInstall() { Log.i(TAG, "Installing app ${patchApp.app.packageName}") @@ -524,7 +571,12 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { if (uninstallFirst) { AlertDialog( - onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, + onDismissRequest = { + onFinish( + LSPPackageManager.STATUS_USER_CANCELLED, + "User cancelled" + ) + }, confirmButton = { TextButton( onClick = { @@ -547,7 +599,12 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { }, dismissButton = { TextButton( - onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, + onClick = { + onFinish( + LSPPackageManager.STATUS_USER_CANCELLED, + "User cancelled" + ) + }, content = { Text(stringResource(android.R.string.cancel)) } ) }, @@ -592,7 +649,30 @@ private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) fun doInstall() { Log.i(TAG, "Installing app ${patchApp.app.packageName}") - installApk(lspApp, lspApp.targetApkFile) + val apkFiles = lspApp.targetApkFiles + if (apkFiles.isNullOrEmpty()){ + Log.e(TAG, "No target APK files found for installation") + onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "No target APK files found for installation") + return + } + if (apkFiles.size > 1) { + scope.launch { + val success = installApks(lspApp, apkFiles) + if (success) { + onFinish( + LSPPackageManager.STATUS_USER_CANCELLED, + "Split APKs installed successfully" + ) + } else { + onFinish( + LSPPackageManager.STATUS_USER_CANCELLED, + "Failed to install split APKs" + ) + } + } + } else { + installApk(lspApp, apkFiles.first()) + } } LaunchedEffect(Unit) { diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt index ccd09bd2f..09e488a31 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt @@ -1,13 +1,23 @@ package org.lsposed.lspatch.ui.util +import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.pm.PackageInstaller import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings import android.util.Log import androidx.compose.foundation.lazy.LazyListState import androidx.core.content.FileProvider import androidx.core.net.toUri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.lsposed.lspatch.BuildConfig import java.io.File +import java.io.IOException val LazyListState.lastVisibleItemIndex get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index @@ -57,4 +67,122 @@ fun uninstallApkByPackageName(context: Context, packageName: String) = try { context.startActivity(intent) } catch (e: Exception) { Log.e("LSPatch", "uninstallApkByPackageName", e) +} + +class InstallResultReceiver : BroadcastReceiver() { + + companion object { + const val ACTION_INSTALL_STATUS = "${BuildConfig.APPLICATION_ID}.INSTALL_STATUS" + + fun createPendingIntent(context: Context, sessionId: Int): PendingIntent { + val intent = Intent(context, InstallResultReceiver::class.java).apply { + action = ACTION_INSTALL_STATUS + } + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + return PendingIntent.getBroadcast(context, sessionId, intent, flags) + } + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_INSTALL_STATUS) { + return + } + + val status = + intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + + when (status) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + val confirmIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT) + if (confirmIntent != null) { + context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + Log.d("lspatch", "请求用户确认安装") + } + + PackageInstaller.STATUS_SUCCESS -> { + // 安装成功 + Log.d("lspatch", "安装完成") + } + + else -> { + // 安装失败 + Log.e("lspatch", "安装失败: $status, $message") + } + } + } +} + +/** + * 安装分包 APK + * @param context 上下文 + * @param apkFiles APK 文件列表 (包括 base.apk 和 split aab 的 apk) + * @return 安装是否成功提交(注意:这不代表最终安装成功,只代表安装请求已发出) + */ + +suspend fun installApks(context: Context, apkFiles: List): Boolean { + // 检查权限:应用是否被允许安装未知来源应用 + if (!context.packageManager.canRequestPackageInstalls()) { + Log.e("lspatch", "没有安装未知来源应用的权限") + // 引导用户去设置页面开启权限 + val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { + data = "package:${context.packageName}".toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + return false + } + + // 检查文件是否存在 + apkFiles.forEach { + if (!it.exists()) { + Log.e("lspatch", "APK 文件不存在: ${it.absolutePath}") + return false + } + } + + return withContext(Dispatchers.IO) { + val packageInstaller = context.packageManager.packageInstaller + var session: PackageInstaller.Session? = null + try { + // 1. 创建安装会话 + val params = + PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + val sessionId = packageInstaller.createSession(params) + session = packageInstaller.openSession(sessionId) + + // 2. 循环将所有 APK 文件写入会话 + apkFiles.forEach { apkFile -> + Log.d("lspatch", "正在添加 APK 到会话: ${apkFile.name}") + session.openWrite(apkFile.name, 0, apkFile.length()).use { outputStream -> + apkFile.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + session.fsync(outputStream) + } + } + } + + // 3. 创建 PendingIntent 用于接收安装结果 + val pendingIntent = InstallResultReceiver.createPendingIntent(context, sessionId) + + // 4. 提交会话,开始安装 + session.commit(pendingIntent.intentSender) + Log.d("lspatch", "安装会话已提交,等待用户确认...") + true + } catch (e: IOException) { + Log.e("lspatch", "安装失败 (IO异常)", e) + // 如果发生错误,放弃会话 + session?.abandon() + false + } catch (e: Exception) { + Log.e("lspatch", "安装失败 (未知异常)", e) + session?.abandon() + false + } + } } \ No newline at end of file diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt index 1ae5cebfd..34ada43ab 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt @@ -18,6 +18,7 @@ import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.PatchConfig import org.lsposed.lspatch.ui.util.installApk +import org.lsposed.lspatch.ui.util.installApks import org.lsposed.lspatch.ui.viewstate.ProcessingState import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo @@ -123,7 +124,16 @@ class AppManageViewModel : ViewModel() { } Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths)) if (!ShizukuApi.isPermissionGranted) { - installApk(lspApp, lspApp.targetApkFile) + val apkFiles = lspApp.targetApkFiles + if (apkFiles.isNullOrEmpty()){ + Log.e(TAG, "No patched APK files found") + throw RuntimeException("No patched APK files found") + } + if (apkFiles.size > 1) { + val success = installApks(lspApp, apkFiles) + } else { + installApk(lspApp, apkFiles.first()) + } } else { val (status, message) = LSPPackageManager.install() if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) From 63a847248295f3dd3ce41ed5f2d80482e0d63409 Mon Sep 17 00:00:00 2001 From: xihan123 Date: Sat, 9 Aug 2025 14:07:14 +0800 Subject: [PATCH 5/5] Remove unnecessary log records and comments --- .../main/java/org/lsposed/lspatch/Patcher.kt | 1 - .../lsposed/lspatch/ui/page/NewPatchScreen.kt | 122 +++++------------- .../java/org/lsposed/lspatch/ui/util/Utils.kt | 30 +---- 3 files changed, 34 insertions(+), 119 deletions(-) diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 4e8f38231..6799f6078 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -65,7 +65,6 @@ object Patcher { val apkFile = File(lspApp.externalCacheDir, apk.name) apk.copyTo(apkFile, overwrite = true) apkFileList.add(apkFile) -// Log.d("lspatch", "Patched file: ${apk.name} -> ${apkFile.absolutePath}") output.use { apk.inputStream().use { input -> input.copyTo(output) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index 7be683944..91e87575a 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -79,27 +79,22 @@ fun NewPatchScreen( val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current val errorUnknown = stringResource(R.string.error_unknown) - val storageLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> - if (apks.isEmpty()) { - navigator.navigateUp() - return@rememberLauncherForActivityResult - } - runBlocking { - LSPPackageManager.getAppInfoFromApks(apks) - .onSuccess { - viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) - } - .onFailure { - lspApp.globalScope.launch { - snackbarHost.showSnackbar( - it.message ?: errorUnknown - ) - } - navigator.navigateUp() - } - } + val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> + if (apks.isEmpty()) { + navigator.navigateUp() + return@rememberLauncherForActivityResult + } + runBlocking { + LSPPackageManager.getAppInfoFromApks(apks) + .onSuccess { + viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) + } + .onFailure { + lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown) } + navigator.navigateUp() + } } + } var showSelectModuleDialog by remember { mutableStateOf(false) } val noXposedModules = stringResource(R.string.patch_no_xposed_module) @@ -164,7 +159,6 @@ fun NewPatchScreen( } } } - PatchState.SELECTING -> { resultRecipient.onNavResult { Log.d(TAG, "onNavResult: $it") @@ -177,7 +171,6 @@ fun NewPatchScreen( } } } - else -> { Scaffold( topBar = { @@ -186,7 +179,6 @@ fun NewPatchScreen( PatchState.PATCHING, PatchState.FINISHED, PatchState.ERROR -> CenterAlignedTopAppBar(title = { Text(viewModel.patchApp.app.packageName) }) - else -> Unit } }, @@ -212,12 +204,10 @@ fun NewPatchScreen( } if (showSelectModuleDialog) { - AlertDialog( - onDismissRequest = { showSelectModuleDialog = false }, + AlertDialog(onDismissRequest = { showSelectModuleDialog = false }, confirmButton = {}, dismissButton = { - TextButton( - content = { Text(stringResource(android.R.string.cancel)) }, + TextButton(content = { Text(stringResource(android.R.string.cancel)) }, onClick = { showSelectModuleDialog = false }) }, title = { @@ -229,8 +219,7 @@ fun NewPatchScreen( }, text = { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - TextButton( - modifier = Modifier.fillMaxWidth(), + TextButton(modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { storageModuleLauncher.launch(arrayOf("application/vnd.android.package-archive")) @@ -242,13 +231,11 @@ fun NewPatchScreen( style = MaterialTheme.typography.bodyLarge ) } - TextButton( - modifier = Modifier.fillMaxWidth(), + TextButton(modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), onClick = { navigator.navigate( - SelectAppsScreenDestination( - true, + SelectAppsScreenDestination(true, viewModel.embeddedModules.mapTo(ArrayList()) { it.app.packageName }) ) showSelectModuleDialog = false @@ -337,12 +324,7 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { extraContent = { TextButton( onClick = onAddEmbed, - content = { - Text( - text = stringResource(R.string.patch_embed_modules), - style = MaterialTheme.typography.bodyLarge - ) - } + content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) } ) } ) @@ -356,9 +338,7 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { title = stringResource(R.string.patch_debuggable) ) SettingsCheckBox( - modifier = Modifier.clickable { - viewModel.overrideVersionCode = !viewModel.overrideVersionCode - }, + modifier = Modifier.clickable { viewModel.overrideVersionCode = !viewModel.overrideVersionCode }, checked = viewModel.overrideVersionCode, icon = Icons.Outlined.Layers, title = stringResource(R.string.patch_override_version_code), @@ -390,9 +370,7 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = viewModel.sigBypassLevel == it, - onClick = { viewModel.sigBypassLevel = it }) + RadioButton(selected = viewModel.sigBypassLevel == it, onClick = { viewModel.sigBypassLevel = it }) Text(sigBypassLvStr(it)) } }, @@ -444,10 +422,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { when (it.first) { Log.DEBUG -> Text(text = it.second) Log.INFO -> Text(text = it.second) - Log.ERROR -> Text( - text = it.second, - color = MaterialTheme.colorScheme.error - ) + Log.ERROR -> Text(text = it.second, color = MaterialTheme.colorScheme.error) } } } @@ -472,26 +447,18 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { scope.launch { installing = 0 if (status == PackageInstaller.STATUS_SUCCESS) { - lspApp.globalScope.launch { - snackbarHost.showSnackbar( - installSuccessfully - ) - } + lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } navigator.navigateUp() } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) { val result = snackbarHost.showSnackbar(installFailed, copyError) if (result == SnackbarResult.ActionPerformed) { - val cm = - lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message)) } } } } - if (installing == 1) InstallDialog( - viewModel.patchApp, - onFinish - ) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish) + if (installing == 1) InstallDialog(viewModel.patchApp, onFinish) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish) Row(Modifier.padding(top = 12.dp)) { Button( modifier = Modifier.weight(1f), @@ -512,7 +479,6 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { ) } } - PatchState.ERROR -> { Row(Modifier.padding(top = 12.dp)) { Button( @@ -524,19 +490,13 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { Button( modifier = Modifier.weight(1f), onClick = { - val cm = - lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip( - ClipData.newPlainText( - "LSPatch", - viewModel.logs.joinToString { it.second + "\n" }) - ) + val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("LSPatch", viewModel.logs.joinToString { it.second + "\n" })) }, content = { Text(stringResource(R.string.copy_error)) } ) } } - else -> Unit } } @@ -546,13 +506,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { @Composable private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { val scope = rememberCoroutineScope() - var uninstallFirst by remember { - mutableStateOf( - ShizukuApi.isPackageInstalledWithoutPatch( - patchApp.app.packageName - ) - ) - } + var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) } var installing by remember { mutableStateOf(0) } suspend fun doInstall() { Log.i(TAG, "Installing app ${patchApp.app.packageName}") @@ -571,12 +525,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { if (uninstallFirst) { AlertDialog( - onDismissRequest = { - onFinish( - LSPPackageManager.STATUS_USER_CANCELLED, - "User cancelled" - ) - }, + onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, confirmButton = { TextButton( onClick = { @@ -599,12 +548,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { }, dismissButton = { TextButton( - onClick = { - onFinish( - LSPPackageManager.STATUS_USER_CANCELLED, - "User cancelled" - ) - }, + onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, content = { Text(stringResource(android.R.string.cancel)) } ) }, @@ -648,10 +592,8 @@ private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) } fun doInstall() { - Log.i(TAG, "Installing app ${patchApp.app.packageName}") val apkFiles = lspApp.targetApkFiles if (apkFiles.isNullOrEmpty()){ - Log.e(TAG, "No target APK files found for installation") onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "No target APK files found for installation") return } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt index 09e488a31..dc3e6e4f3 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt @@ -102,34 +102,19 @@ class InstallResultReceiver : BroadcastReceiver() { if (confirmIntent != null) { context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } - Log.d("lspatch", "请求用户确认安装") } PackageInstaller.STATUS_SUCCESS -> { - // 安装成功 - Log.d("lspatch", "安装完成") } else -> { - // 安装失败 - Log.e("lspatch", "安装失败: $status, $message") } } } } -/** - * 安装分包 APK - * @param context 上下文 - * @param apkFiles APK 文件列表 (包括 base.apk 和 split aab 的 apk) - * @return 安装是否成功提交(注意:这不代表最终安装成功,只代表安装请求已发出) - */ - suspend fun installApks(context: Context, apkFiles: List): Boolean { - // 检查权限:应用是否被允许安装未知来源应用 if (!context.packageManager.canRequestPackageInstalls()) { - Log.e("lspatch", "没有安装未知来源应用的权限") - // 引导用户去设置页面开启权限 val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { data = "package:${context.packageName}".toUri() flags = Intent.FLAG_ACTIVITY_NEW_TASK @@ -138,10 +123,8 @@ suspend fun installApks(context: Context, apkFiles: List): Boolean { return false } - // 检查文件是否存在 apkFiles.forEach { if (!it.exists()) { - Log.e("lspatch", "APK 文件不存在: ${it.absolutePath}") return false } } @@ -150,15 +133,12 @@ suspend fun installApks(context: Context, apkFiles: List): Boolean { val packageInstaller = context.packageManager.packageInstaller var session: PackageInstaller.Session? = null try { - // 1. 创建安装会话 val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) val sessionId = packageInstaller.createSession(params) session = packageInstaller.openSession(sessionId) - // 2. 循环将所有 APK 文件写入会话 apkFiles.forEach { apkFile -> - Log.d("lspatch", "正在添加 APK 到会话: ${apkFile.name}") session.openWrite(apkFile.name, 0, apkFile.length()).use { outputStream -> apkFile.inputStream().use { inputStream -> inputStream.copyTo(outputStream) @@ -167,20 +147,14 @@ suspend fun installApks(context: Context, apkFiles: List): Boolean { } } - // 3. 创建 PendingIntent 用于接收安装结果 val pendingIntent = InstallResultReceiver.createPendingIntent(context, sessionId) - // 4. 提交会话,开始安装 session.commit(pendingIntent.intentSender) - Log.d("lspatch", "安装会话已提交,等待用户确认...") true - } catch (e: IOException) { - Log.e("lspatch", "安装失败 (IO异常)", e) - // 如果发生错误,放弃会话 + } catch (_: IOException) { session?.abandon() false - } catch (e: Exception) { - Log.e("lspatch", "安装失败 (未知异常)", e) + } catch (_: Exception) { session?.abandon() false }