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>
This commit is contained in:
NkBe(HSSkyBoy) 2025-10-05 22:55:38 +08:00 committed by NkBe
parent 9a58d53314
commit d644c22ade
No known key found for this signature in database
GPG Key ID: 525137026FF031DF
14 changed files with 416 additions and 188 deletions

View File

@ -1,3 +1,5 @@
android.experimental.enableNewResourceShrinker.preciseShrinking=true
android.enableAppCompileTimeRClass=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

View File

@ -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<String>,
@ -24,8 +25,8 @@ object Patcher {
) {
fun toStringArray(): Array<String> {
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<File>()
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}")

View File

@ -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 = {
},
)
}
}

View File

@ -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<NewPatchViewModel>()
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 {
scope.launch {
LSPPackageManager.getAppInfoFromApks(apks).onSuccess { appInfos ->
val modules = appInfos.filter { it.isXposedModule }
if (modules.isEmpty()) {
snackbarHost.showSnackbar(noXposedModules)
}
return@onSuccess
} 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 ->
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)) }
)
@ -519,51 +520,21 @@ 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 installing by remember { mutableStateOf(0) }
suspend fun doInstall() {
Log.i(TAG, "Installing app ${patchApp.app.packageName}")
installing = 1
val (status, message) = LSPPackageManager.install()
installing = 0
Log.i(TAG, "Installation end: $status, $message")
onFinish(status, message)
}
LaunchedEffect(Unit) {
if (!uninstallFirst) {
doInstall()
}
}
if (uninstallFirst) {
private fun UninstallConfirmationDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
onDismissRequest = onDismiss,
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)
}
}
},
onClick = onConfirm,
content = { Text(stringResource(android.R.string.ok)) }
)
},
dismissButton = {
TextButton(
onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
onClick = onDismiss,
content = { Text(stringResource(android.R.string.cancel)) }
)
},
@ -578,6 +549,47 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
)
}
@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) } // 0: idle, 1: installing, 2: uninstalling
suspend fun doInstall() {
Log.i(TAG, "Installing app ${patchApp.app.packageName}")
installing = 1
val (status, message) = LSPPackageManager.install()
installing = 0
Log.i(TAG, "Installation end: $status, $message")
onFinish(status, message)
}
LaunchedEffect(uninstallFirst) {
if (!uninstallFirst && installing == 0) {
doInstall()
}
}
if (uninstallFirst) {
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)
}
}
}
)
}
if (installing != 0) {
AlertDialog(
onDismissRequest = {},
@ -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"
if (success) PackageInstaller.STATUS_SUCCESS else PackageInstaller.STATUS_FAILURE,
if (success) "Split APKs installed successfully" else "Failed to install split APKs"
)
} else {
onFinish(
PackageInstaller.STATUS_FAILURE,
"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) {
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(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED)
context.registerReceiver(splitInstallReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS))
}
context.registerReceiver(splitInstallReceiver, intentFilter)
}
override fun onDestroy(owner: LifecycleOwner) {
onDispose {
context.unregisterReceiver(splitInstallReceiver)
}
}
override fun onResume(owner: LifecycleOwner) {
LaunchedEffect(uninstallFirst) {
if (!uninstallFirst) {
Log.d(TAG,"Starting installation without uninstalling first")
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled")
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")
}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
if (uninstallFirst) {
AlertDialog(
onDismissRequest = {
onFinish(
LSPPackageManager.STATUS_USER_CANCELLED,
"User cancelled"
)
},
confirmButton = {
TextButton(
onClick = {
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Reset")
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
}
},
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)) }
}
)
}
}

View File

@ -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()) }
)

View File

@ -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()){

View File

@ -63,6 +63,8 @@
<string name="patch_sigbypasslv2">lv2: 绕过 PM + openat (libc)</string>
<string name="patch_override_version_code">覆写版本号</string>
<string name="patch_override_version_code_desc">将修补的 App 版本号重写为 1\n这将允许后续降级安装并且通常来说这不会影响应用实际感知到的版本号</string>
<string name="patch_new_package">修补新包名</string>
<string name="hint_patch_new_package">请输入新的包名</string>
<string name="patch_inject_dex">注入加载器 Dex</string>
<string name="patch_inject_dex_desc">对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行</string>
<string name="patch_output_log_to_media">日志输出到 Media 目录</string>

View File

@ -63,6 +63,8 @@
<string name="patch_sigbypasslv2">lv2: 繞過 PM + openat (libc)</string>
<string name="patch_override_version_code">覆蓋版本編號</string>
<string name="patch_override_version_code_desc">將打包應用程式的版本編號改成 1\n允許以後降級安裝一般來說這不會影響應用程式實際感知的版本編號。</string>
<string name="patch_new_package">修補新套件名</string>
<string name="hint_patch_new_package">請輸入新的套件名</string>
<string name="patch_inject_dex">注入載入器 Dex</string>
<string name="patch_inject_dex_desc">對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行</string>
<string name="patch_output_log_to_media">日誌輸出到 Media 目錄</string>

View File

@ -63,6 +63,8 @@
<string name="patch_sigbypasslv0">lv0: Off</string>
<string name="patch_sigbypasslv1">lv1: Bypass PM</string>
<string name="patch_sigbypasslv2">lv2: Bypass PM + openat (libc)</string>
<string name="patch_new_package">Patch New PackageName</string>
<string name="hint_patch_new_package">Input a new package for app</string>
<string name="patch_override_version_code">Override version code</string>
<string name="patch_override_version_code_desc">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</string>
<string name="patch_inject_dex">Inject loader dex</string>

View File

@ -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);

View File

@ -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<String> 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<String> 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<String> replacePermissionWithNewPackage(List<String> list, String pkg, String newPackage){
List<String> 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<String> replaceUsesPermissionWithNewPackage(List<String> list, String pkg, String newPackage){
List<String> 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<String> replaceProviderWithNewPackage(List<String> list, String pkg, String newPackage){
List<String> 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<String> permissions, List<String> uses_permissions, List<String> 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<String>() {
@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)

View File

@ -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<String> permissions = new ArrayList<>();
List<String> use_permissions = new ArrayList<>();
List<String> 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<String> permissions;
public List<String> use_permissions;
public List<String> authorities;
public Pair(String packageName, String appComponentFactory, int minSdkVersion) {
this.packageName = packageName;
this.appComponentFactory = appComponentFactory;
this.minSdkVersion = minSdkVersion;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
public List<String> getUse_permissions() {
return use_permissions;
}
public void setUse_permissions(List<String> use_permissions) {
this.use_permissions = use_permissions;
}
public List<String> getAuthorities() {
return authorities;
}
public void setAuthorities(List<String> authorities) {
this.authorities = authorities;
}
}
}

View File

@ -29,7 +29,7 @@ dependencyResolutionManagement {
}
}
rootProject.name = "LSPatch"
rootProject.name = "NPatch"
include(
":apache",
":apkzlib",

View File

@ -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;
}
}