From d644c22ade3b9cb43545704d699d13a9239d5659 Mon Sep 17 00:00:00 2001 From: "NkBe(HSSkyBoy)" Date: Sun, 5 Oct 2025 22:55:38 +0800 Subject: [PATCH] feat: support patching with new package name feat: log APK patch paths in submitPatch fix: improve temp APK file handling feat: add newPackageName option refactor: optimize patch install dialogs feat: support rename packagename Co-Authored-By: javaeryang <27242250+javaeryang@users.noreply.github.com> --- gradle.properties | 2 + .../main/java/org/lsposed/lspatch/Patcher.kt | 30 +- .../lspatch/ui/component/settings/Editor.kt | 67 +++++ .../lsposed/lspatch/ui/page/NewPatchScreen.kt | 283 ++++++++---------- .../lspatch/ui/viewmodel/NewPatchViewModel.kt | 9 +- .../ui/viewmodel/manage/AppManageViewModel.kt | 4 +- .../src/main/res/values-zh-rCN/strings.xml | 2 + .../src/main/res/values-zh-rTW/strings.xml | 2 + manager/src/main/res/values/strings.xml | 2 + .../lspatch/loader/LSPApplication.java | 2 +- .../main/java/org/lsposed/patch/LSPatch.java | 113 ++++++- .../lsposed/patch/util/ManifestParser.java | 77 ++++- settings.gradle.kts | 4 +- .../lsposed/lspatch/share/PatchConfig.java | 7 +- 14 files changed, 416 insertions(+), 188 deletions(-) create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Editor.kt diff --git a/gradle.properties b/gradle.properties index 387da39..5158a2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ android.experimental.enableNewResourceShrinker.preciseShrinking=true android.enableAppCompileTimeRClass=true android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + \ No newline at end of file diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 30757ec..300844b 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -17,6 +17,7 @@ import java.util.Collections.addAll object Patcher { class Options( + val newPackageName: String, private val injectDex: Boolean, private val config: PatchConfig, private val apkPaths: List, @@ -24,8 +25,8 @@ object Patcher { ) { fun toStringArray(): Array { return buildList { - addAll(apkPaths) add("-o"); add(lspApp.tmpApkDir.absolutePath) + add("-p"); add(config.newPackage) if (config.debuggable) add("-d") add("-l"); add(config.sigBypassLevel.toString()) if (config.useManager) add("--manager") @@ -38,6 +39,7 @@ object Patcher { if (!MyKeyStore.useDefault) { addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword)) } + addAll(apkPaths) }.toTypedArray() } } @@ -56,20 +58,22 @@ object Patcher { lspApp.targetApkFiles?.clear() val apkFileList = arrayListOf() lspApp.tmpApkDir.walk() - .filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } - .forEach { apk -> - val file = root.createFile("application/vnd.android.package-archive", apk.name) - ?: 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) - apkFileList.add(apkFile) - output.use { - apk.inputStream().use { input -> + .filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } + .forEach { tempApkFile -> + val cachedApkFile = File(lspApp.externalCacheDir, tempApkFile.name) + if (tempApkFile.renameTo(cachedApkFile).not()) { + tempApkFile.copyTo(cachedApkFile, overwrite = true) + tempApkFile.delete() + } + apkFileList.add(cachedApkFile) + + val finalFile = root.createFile("application/vnd.android.package-archive", cachedApkFile.name) + ?: throw IOException("無法建立輸出檔案: ${cachedApkFile.name}") + lspApp.contentResolver.openOutputStream(finalFile.uri)?.use { output -> + cachedApkFile.inputStream().use { input -> input.copyTo(output) } - } + } ?: throw IOException("Unable to open an output stream: ${finalFile.uri}") } lspApp.targetApkFiles = apkFileList logger.i("Patched files are saved to ${root.uri.lastPathSegment}") diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Editor.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Editor.kt new file mode 100644 index 0000000..1649b49 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Editor.kt @@ -0,0 +1,67 @@ +package org.lsposed.lspatch.ui.component.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsEditor( + modifier: Modifier, + label: String, + text: String, + onValueChange: (String) -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(Modifier.weight(1f).padding(vertical = 6.dp)) { + Column { + OutlinedTextField( + value = text, + label = { Text(label) }, + onValueChange = onValueChange, + textStyle = TextStyle(fontWeight = FontWeight.Bold), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + focusedContainerColor = MaterialTheme.colorScheme.surface + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) + } + } + } +} + +@Preview +@Composable +private fun SettingsCheckBoxPreview() { + Column { + SettingsEditor( + Modifier.padding(horizontal = 8.dp), + "标签", + "编辑框文字", + onValueChange = { + + }, + ) + } +} \ No newline at end of file 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 11995b1..9f3c932 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 @@ -34,7 +34,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.DefaultLifecycleObserver @@ -46,13 +48,13 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.lsposed.lspatch.R import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.SelectionColumn import org.lsposed.lspatch.ui.component.ShimmerAnimation import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox +import org.lsposed.lspatch.ui.component.settings.SettingsEditor import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.util.InstallResultReceiver @@ -87,19 +89,20 @@ fun NewPatchScreen( ) { val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() val errorUnknown = stringResource(R.string.error_unknown) val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> if (apks.isEmpty()) { navigator.navigateUp() return@rememberLauncherForActivityResult } - runBlocking { + scope.launch { LSPPackageManager.getAppInfoFromApks(apks) .onSuccess { viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) } .onFailure { - lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown) } + snackbarHost.showSnackbar(it.message ?: errorUnknown) navigator.navigateUp() } } @@ -112,20 +115,16 @@ fun NewPatchScreen( if (apks.isEmpty()) { return@rememberLauncherForActivityResult } - runBlocking { - LSPPackageManager.getAppInfoFromApks(apks).onSuccess { it -> - viewModel.embeddedModules = it.filter { it.isXposedModule }.ifEmpty { - lspApp.globalScope.launch { - snackbarHost.showSnackbar(noXposedModules) - } - return@onSuccess + scope.launch { + LSPPackageManager.getAppInfoFromApks(apks).onSuccess { appInfos -> + val modules = appInfos.filter { it.isXposedModule } + if (modules.isEmpty()) { + snackbarHost.showSnackbar(noXposedModules) + } else { + viewModel.embeddedModules = modules } }.onFailure { - lspApp.globalScope.launch { - snackbarHost.showSnackbar( - it.message ?: errorUnknown - ) - } + snackbarHost.showSnackbar(it.message ?: errorUnknown) } } } @@ -147,16 +146,12 @@ fun NewPatchScreen( } ACTION_INTENT_INSTALL -> { - runBlocking { - data?.let { uri -> + data?.let { uri -> + scope.launch { LSPPackageManager.getAppInfoFromApks(listOf(uri)).onSuccess { viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) }.onFailure { - lspApp.globalScope.launch { - snackbarHost.showSnackbar( - it.message ?: errorUnknown - ) - } + snackbarHost.showSnackbar(it.message ?: errorUnknown) navigator.navigateUp() } } @@ -335,6 +330,13 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { } ) } + SettingsEditor(Modifier.padding(top = 6.dp), + stringResource(R.string.patch_new_package), + viewModel.newPackageName, + onValueChange = { + viewModel.newPackageName = it + }, + ) SettingsCheckBox( modifier = Modifier .padding(top = 6.dp) @@ -430,20 +432,19 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { .fillMaxWidth() .heightIn(max = shellBoxMaxHeight) .clip(RoundedCornerShape(32.dp)) - .background(brush) + .background(MaterialTheme.colorScheme.surfaceVariant) // Replaced 'brush' with a theme color .padding(horizontal = 24.dp, vertical = 18.dp) ) { items(viewModel.logs) { when (it.first) { - Log.DEBUG -> Text(text = it.second) - Log.INFO -> Text(text = it.second) + Log.DEBUG, Log.INFO -> Text(text = it.second) Log.ERROR -> Text(text = it.second, color = MaterialTheme.colorScheme.error) } } } LaunchedEffect(scrollState.lastItemIndex) { - if (!scrollState.isScrolledToEnd) { + if (scrollState.lastItemIndex != null && !scrollState.isScrolledToEnd) { scrollState.animateScrollToItem(scrollState.lastItemIndex!!) } } @@ -453,7 +454,6 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { when (viewModel.patchState) { PatchState.PATCHING -> BackHandler {} PatchState.FINISHED -> { - val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) val installSuccessfully = stringResource(R.string.patch_install_successfully) val installFailed = stringResource(R.string.patch_install_failed) val copyError = stringResource(R.string.copy_error) @@ -461,7 +461,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { val onFinish: (Int, String?) -> Unit = { status, message -> scope.launch { if (status == PackageInstaller.STATUS_SUCCESS) { - lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } + snackbarHost.showSnackbar(installSuccessfully) navigator.navigateUp() } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) { val result = snackbarHost.showSnackbar(installFailed, copyError) @@ -470,6 +470,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message)) } } + installation = null // Reset installation state } } when (installation) { @@ -506,7 +507,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { 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" })) + cm.setPrimaryClip(ClipData.newPlainText("LSPatch", viewModel.logs.joinToString(separator = "\n") { it.second })) }, content = { Text(stringResource(R.string.copy_error)) } ) @@ -518,11 +519,42 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { } } +@Composable +private fun UninstallConfirmationDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = onConfirm, + content = { Text(stringResource(android.R.string.ok)) } + ) + }, + dismissButton = { + TextButton( + onClick = onDismiss, + 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)) } + ) +} + @Composable private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { val scope = rememberCoroutineScope() var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) } - var installing by remember { mutableStateOf(0) } + var installing by remember { mutableStateOf(0) } // 0: idle, 1: installing, 2: uninstalling + suspend fun doInstall() { Log.i(TAG, "Installing app ${patchApp.app.packageName}") installing = 1 @@ -532,49 +564,29 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { onFinish(status, message) } - LaunchedEffect(Unit) { - if (!uninstallFirst) { + LaunchedEffect(uninstallFirst) { + if (!uninstallFirst && installing == 0) { doInstall() } } if (uninstallFirst) { - AlertDialog( - onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, - confirmButton = { - TextButton( - onClick = { - scope.launch { - Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") - uninstallFirst = false - installing = 2 - val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName) - installing = 0 - Log.i(TAG, "Uninstallation end: $status, $message") - if (status == PackageInstaller.STATUS_SUCCESS) { - doInstall() - } else { - onFinish(status, message) - } - } - }, - 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)) } + UninstallConfirmationDialog( + onDismiss = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, + onConfirm = { + scope.launch { + Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") + installing = 2 + val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName) + installing = 0 + Log.i(TAG, "Uninstallation end: $status, $message") + if (status == PackageInstaller.STATUS_SUCCESS) { + uninstallFirst = false // This will trigger the LaunchedEffect to install + } else { + onFinish(status, message) + } + } + } ) } @@ -589,6 +601,15 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { fontFamily = FontFamily.Serif, textAlign = TextAlign.Center ) + }, + text = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator(modifier = Modifier.padding(16.dp)) + } } ) } @@ -597,19 +618,13 @@ 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 - ) - ) - } - val lifecycleOwner = LocalLifecycleOwner.current + var uninstallFirst by remember { mutableStateOf(checkIsApkFixedByLSP(lspApp, patchApp.app.packageName)) } val context = LocalContext.current - val splitInstallReceiver by lazy { InstallResultReceiver() } + val lifecycleOwner = LocalLifecycleOwner.current + val splitInstallReceiver = remember { InstallResultReceiver() } + fun doInstall() { - Log.i(TAG, "Installing app ${patchApp.app.packageName}") + Log.i(TAG, "Installing app with system installer: ${patchApp.app.packageName}") val apkFiles = lspApp.targetApkFiles if (apkFiles.isNullOrEmpty()){ onFinish(PackageInstaller.STATUS_FAILURE, "No target APK files found for installation") @@ -618,91 +633,53 @@ private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) if (apkFiles.size > 1) { scope.launch { val success = installApks(lspApp, apkFiles) - if (success) { - onFinish( - PackageInstaller.STATUS_SUCCESS, - "Split APKs installed successfully" - ) - } else { - onFinish( - PackageInstaller.STATUS_FAILURE, - "Failed to install split APKs" - ) - } + onFinish( + if (success) PackageInstaller.STATUS_SUCCESS else PackageInstaller.STATUS_FAILURE, + if (success) "Split APKs installed successfully" else "Failed to install split APKs" + ) } } else { installApk(lspApp, apkFiles.first()) + // For single APK install, the result is typically handled by onActivityResult, + // but since we are using a receiver for splits, we can unify later if needed. + // For now, system prompt is the feedback. We might need a better way to track this. } } - DisposableEffect(lifecycleOwner) { - val observer = object : DefaultLifecycleObserver { - @SuppressLint("UnspecifiedRegisterReceiverFlag") - override fun onCreate(owner: LifecycleOwner) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED) - } else { - context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS)) - } - } - - override fun onDestroy(owner: LifecycleOwner) { - context.unregisterReceiver(splitInstallReceiver) - } - - override fun onResume(owner: LifecycleOwner) { - if (!uninstallFirst) { - Log.d(TAG,"Starting installation without uninstalling first") - onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") - doInstall() - } - } + DisposableEffect(lifecycleOwner, context) { + val intentFilter = IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS) + // Correctly handle receiver registration for different Android versions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(splitInstallReceiver, intentFilter, RECEIVER_NOT_EXPORTED) + } else { + context.registerReceiver(splitInstallReceiver, intentFilter) } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + onDispose { + context.unregisterReceiver(splitInstallReceiver) + } + } + + LaunchedEffect(uninstallFirst) { + if (!uninstallFirst) { + Log.d(TAG, "State changed to install, starting installation via system.") + doInstall() + // Since system installer is an Intent, it's fire-and-forget. We can dismiss our UI. + onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Handed over to system installer") + } } 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)) } + UninstallConfirmationDialog( + onDismiss = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, + onConfirm = { + scope.launch { + Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") + uninstallApkByPackageName(lspApp, patchApp.app.packageName) + // After uninstall intent is sent, we can assume it will proceed. + uninstallFirst = false + } + } ) } } \ No newline at end of file diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt index 795536b..f0b7107 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt @@ -38,7 +38,9 @@ class NewPatchViewModel : ViewModel() { var patchState by mutableStateOf(PatchState.INIT) private set + // Patch Configuration var useManager by mutableStateOf(true) + var newPackageName by mutableStateOf("") var debuggable by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false) var sigBypassLevel by mutableStateOf(2) @@ -90,14 +92,17 @@ class NewPatchViewModel : ViewModel() { Log.d(TAG, "Configuring patch for ${app.app.packageName}") patchApp = app patchState = PatchState.CONFIGURING + newPackageName = app.app.packageName } private fun submitPatch() { Log.d(TAG, "Submit patch") if (useManager) embeddedModules = emptyList() + val config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, outputLog, newPackageName) patchOptions = Patcher.Options( + newPackageName = newPackageName, injectDex = injectDex, - config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, outputLog), + config = config, apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()), embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } ) @@ -117,4 +122,4 @@ class NewPatchViewModel : ViewModel() { LSPPackageManager.cleanTmpApkDir() } } -} +} \ 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 464275c..e533c7b 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 @@ -125,7 +125,7 @@ class AppManageViewModel : ViewModel() { } } } - Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths)) + Patcher.patch(logger, Patcher.Options(appInfo.app.packageName, false, config, patchPaths, embeddedModulePaths)) if (!ShizukuApi.isPermissionGranted) { val apkFiles = lspApp.targetApkFiles if (apkFiles.isNullOrEmpty()){ @@ -154,4 +154,4 @@ class AppManageViewModel : ViewModel() { } optimizeState = ProcessingState.Done(result) } -} +} \ No newline at end of file diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index bb31d26..9d21c84 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -63,6 +63,8 @@ lv2: 绕过 PM + openat (libc) 覆写版本号 将修补的 App 版本号重写为 1\n这将允许后续降级安装,并且通常来说这不会影响应用实际感知到的版本号 + 修补新包名 + 请输入新的包名 注入加载器 Dex 对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行 日志输出到 Media 目录 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index b0e53e2..5c7e7e9 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -63,6 +63,8 @@ lv2: 繞過 PM + openat (libc) 覆蓋版本編號 將打包應用程式的版本編號改成 1\n允許以後降級安裝,一般來說,這不會影響應用程式實際感知的版本編號。 + 修補新套件名 + 請輸入新的套件名 注入載入器 Dex 對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行 日誌輸出到 Media 目錄 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index b18ad5b..24684df 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -63,6 +63,8 @@ lv0: Off lv1: Bypass PM lv2: Bypass PM + openat (libc) + Patch New PackageName + Input a new package for app Override version code Override the patched app\'s version code to 1\nThis allows downgrade installation in the future, and generally this will not affect the version code actually perceived by the application Inject loader dex 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 1bd240e..471f68f 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 @@ -139,7 +139,7 @@ public class LSPApplication { BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); config = new Gson().fromJson(streamReader, PatchConfig.class); } catch (IOException e) { - Log.e(TAG, "Failed to load config file"); + Log.e(TAG, "Failed to load config file", e); return null; } Log.i(TAG, "Use manager: " + config.useManager); diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index d86e723..a66f75d 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -18,8 +18,12 @@ import com.beust.jcommander.ParameterException; import com.google.gson.Gson; import com.wind.meditor.core.ManifestEditor; import com.wind.meditor.property.AttributeItem; +import com.wind.meditor.property.AttributeMapper; import com.wind.meditor.property.ModificationProperty; +import com.wind.meditor.property.PermissionMapper; import com.wind.meditor.utils.NodeValue; +import com.wind.meditor.utils.PermissionType; +import com.wind.meditor.utils.Utils; import org.apache.commons.io.FilenameUtils; import org.lsposed.lspatch.share.Constants; @@ -43,9 +47,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.UUID; import java.util.stream.Collectors; public class LSPatch { @@ -99,6 +106,8 @@ public class LSPatch { @Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk") private List modules = new ArrayList<>(); + @Parameter(names = {"-p", "--newpackage"}, description = "Patch with new package") + private String newPackageName = ""; private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; private static final HashSet ARCHES = new HashSet<>(Arrays.asList( @@ -226,17 +235,32 @@ public class LSPatch { if (manifestEntry == null) throw new PatchError("Provided file is not a valid apk"); + String newPackage = newPackageName; + // parse the app appComponentFactory full name from the manifest file final String appComponentFactory; int minSdkVersion; + ManifestParser.Pair pair; try (var is = manifestEntry.open()) { - var pair = ManifestParser.parseManifestFile(is); + pair = ManifestParser.parseManifestFile(is); if (pair == null) throw new PatchError("Failed to parse AndroidManifest.xml"); appComponentFactory = pair.appComponentFactory; minSdkVersion = pair.minSdkVersion; logger.d("original appComponentFactory class: " + appComponentFactory); logger.d("original minSdkVersion: " + minSdkVersion); + + if (newPackage == null || newPackage.isEmpty()){ + newPackage = pair.packageName; + } + + logger.i("permissions: " + pair.permissions); + logger.i("use-permissions: " +pair.use_permissions); + logger.i("provider.authorities: " + pair.authorities); + + logger.i("permissions size: " + (pair.permissions == null ? 0 : pair.permissions.size())); + logger.i("use-permissions size: " + (pair.use_permissions == null ? 0 : pair.use_permissions.size())); + logger.i("authorities size: " + (pair.authorities == null ? 0 : pair.authorities.size())); } final boolean skipSplit = apkPaths.size() > 1 && srcApkFile.getName().startsWith("split_") && appComponentFactory == null; @@ -254,10 +278,10 @@ public class LSPatch { logger.i("Patching apk..."); // modify manifest - final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory, outputLog); + final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory, outputLog, newPackage); final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); final var metadata = Base64.getEncoder().encodeToString(configBytes); - try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) { + try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion, pair.packageName, newPackage, pair.permissions, pair.use_permissions, pair.authorities))) { dstZFile.add(ANDROID_MANIFEST_XML, is); } catch (Throwable e) { throw new PatchError("Error when modifying manifest", e); @@ -331,6 +355,57 @@ public class LSPatch { logger.i("Done. Output APK: " + outputFile.getAbsolutePath()); } + private List replacePermissionWithNewPackage(List list, String pkg, String newPackage){ + List res = new LinkedList<>(); + if (list != null && !list.isEmpty()){ + for (String next : list) { + if (next != null && !next.isEmpty()) { + if (next.startsWith(pkg)){ + String s = next.replaceAll(pkg, newPackage); + res.add(s); + }else { + res.add(newPackage + "_" + next); + } + } + } + } + return res; + } + + private List replaceUsesPermissionWithNewPackage(List list, String pkg, String newPackage){ + List res = new LinkedList<>(); + if (list != null && !list.isEmpty()){ + for (String next : list) { + if (next != null && !next.isEmpty()) { + if (next.startsWith(pkg)){ + String s = next.replaceAll(pkg, newPackage); + res.add(s); + }else { + res.add(newPackage + "_" + next); + } + } + } + } + return res; + } + + private List replaceProviderWithNewPackage(List list, String pkg, String newPackage){ + List res = new LinkedList<>(); + if (list != null && !list.isEmpty()){ + for (String next : list) { + if (next != null && !next.isEmpty()) { + if (next.startsWith(pkg)){ + String s = next.replaceAll(pkg, newPackage); + res.add(s); + }else { + res.add(newPackage + "_" + next); + } + } + } + } + return res; + } + private void embedModules(ZFile zFile) { for (var module : modules) { File file = new File(module); @@ -343,12 +418,12 @@ public class LSPatch { logger.i(" - " + packageName); zFile.add(EMBEDDED_MODULES_ASSET_PATH + packageName + ".apk", fileIs); } catch (NullPointerException | IOException e) { - logger.e(module + " does not exist or is not a valid apk file."); + logger.e(module + " does not exist or is not a valid apk file. error:" + e); } } } - private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion) throws IOException { + private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion, String originPackage, String newPackage, List permissions, List uses_permissions, List authorities) throws IOException { ModificationProperty property = new ModificationProperty(); if (overrideVersionCode) @@ -357,6 +432,34 @@ public class LSPatch { property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, 27)); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); + if (newPackage != null && !newPackage.isEmpty()){ + property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackage).setNamespace(null)); + } + property.setPermissionMapper(new PermissionMapper() { + @Override + public String map(PermissionType type, String permission) { + if (permission.startsWith(originPackage)){ + assert newPackage != null; + return permission.replaceFirst(originPackage, newPackage); + } + if (permission.startsWith("android") + || permission.startsWith("com.android")){ + return permission; + } + return newPackage + "_" + permission; + } + }); + property.setAuthorityMapper(new AttributeMapper() { + @Override + public String map(String value) { + if (value.startsWith(originPackage)){ + assert newPackage != null; + return value.replaceFirst(originPackage, newPackage); + } + return newPackage + "_" + value; + } + }); + property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata)); // TODO: replace query_all with queries -> manager if (useManager) diff --git a/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java b/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java index 5ee2e60..9710201 100644 --- a/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java +++ b/patch/src/main/java/org/lsposed/patch/util/ManifestParser.java @@ -6,6 +6,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import pxb.android.axml.AxmlParser; @@ -19,6 +21,9 @@ public class ManifestParser { String packageName = null; String appComponentFactory = null; int minSdkVersion = 0; + List permissions = new ArrayList<>(); + List use_permissions = new ArrayList<>(); + List authorities = new ArrayList<>(); try { while (true) { @@ -46,16 +51,41 @@ public class ManifestParser { } } + if ("permission".equals(name)){ + if ("name".equals(attrName)){ + String permissionName = parser.getAttrValue(i).toString(); + if (!permissionName.startsWith("android")){ + permissions.add(permissionName); + } + } + } + + if ("uses-permission".equals(name)){ + if ("name".equals(attrName)){ + String permissionName = parser.getAttrValue(i).toString(); + if (!permissionName.startsWith("android")){ + use_permissions.add(permissionName); + } + } + } + + if ("provider".equals(name)){ + if ("authorities".equals(attrName)){ + String authority = parser.getAttrValue(i).toString(); + authorities.add(authority); + } + } + if ("appComponentFactory".equals(attrName) || attrNameRes == 0x0101057a) { appComponentFactory = parser.getAttrValue(i).toString(); } - if (packageName != null && packageName.length() > 0 && - appComponentFactory != null && appComponentFactory.length() > 0 && - minSdkVersion > 0 - ) { - return new Pair(packageName, appComponentFactory, minSdkVersion); - } +// if (packageName != null && packageName.length() > 0 && +// appComponentFactory != null && appComponentFactory.length() > 0 && +// minSdkVersion > 0 +// ) { +// return new Pair(packageName, appComponentFactory, minSdkVersion); +// } } } else if (type == AxmlParser.END_TAG) { // ignored @@ -65,7 +95,11 @@ public class ManifestParser { return null; } - return new Pair(packageName, appComponentFactory, minSdkVersion); + Pair pair = new Pair(packageName, appComponentFactory, minSdkVersion); + pair.setPermissions(permissions); + pair.setUse_permissions(use_permissions); + pair.setAuthorities(authorities); + return pair; } /** @@ -83,12 +117,39 @@ public class ManifestParser { public String appComponentFactory; public int minSdkVersion; + public List permissions; + public List use_permissions; + public List authorities; public Pair(String packageName, String appComponentFactory, int minSdkVersion) { this.packageName = packageName; this.appComponentFactory = appComponentFactory; this.minSdkVersion = minSdkVersion; } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + public List getUse_permissions() { + return use_permissions; + } + + public void setUse_permissions(List use_permissions) { + this.use_permissions = use_permissions; + } + + public List getAuthorities() { + return authorities; + } + + public void setAuthorities(List authorities) { + this.authorities = authorities; + } } -} +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9364f78..a429c09 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,7 +29,7 @@ dependencyResolutionManagement { } } -rootProject.name = "LSPatch" +rootProject.name = "NPatch" include( ":apache", ":apkzlib", @@ -58,4 +58,4 @@ project(":services:daemon-service").projectDir = file("core/services/daemon-serv project(":services:manager-service").projectDir = file("core/services/manager-service") project(":services:xposed-service:interface").projectDir = file("core/services/xposed-service/interface") -buildCache { local { removeUnusedEntriesAfterDays = 1 } } +buildCache { local { removeUnusedEntriesAfterDays = 1 } } \ No newline at end of file diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java index a732291..36816c2 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java @@ -11,6 +11,7 @@ public class PatchConfig { public final String appComponentFactory; public final LSPConfig lspConfig; public final String managerPackageName; + public final String newPackage; public PatchConfig( boolean useManager, @@ -19,7 +20,8 @@ public class PatchConfig { int sigBypassLevel, String originalSignature, String appComponentFactory, - boolean outputLog + boolean outputLog, + String newPackage ) { this.useManager = useManager; this.debuggable = debuggable; @@ -29,6 +31,7 @@ public class PatchConfig { this.appComponentFactory = appComponentFactory; this.lspConfig = LSPConfig.instance; this.managerPackageName = Constants.MANAGER_PACKAGE_NAME; + this.newPackage = newPackage; this.outputLog = outputLog; } -} +} \ No newline at end of file