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.experimental.enableNewResourceShrinker.preciseShrinking=true
android.enableAppCompileTimeRClass=true android.enableAppCompileTimeRClass=true
android.useAndroidX=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 { object Patcher {
class Options( class Options(
val newPackageName: String,
private val injectDex: Boolean, private val injectDex: Boolean,
private val config: PatchConfig, private val config: PatchConfig,
private val apkPaths: List<String>, private val apkPaths: List<String>,
@ -24,8 +25,8 @@ object Patcher {
) { ) {
fun toStringArray(): Array<String> { fun toStringArray(): Array<String> {
return buildList { return buildList {
addAll(apkPaths)
add("-o"); add(lspApp.tmpApkDir.absolutePath) add("-o"); add(lspApp.tmpApkDir.absolutePath)
add("-p"); add(config.newPackage)
if (config.debuggable) add("-d") if (config.debuggable) add("-d")
add("-l"); add(config.sigBypassLevel.toString()) add("-l"); add(config.sigBypassLevel.toString())
if (config.useManager) add("--manager") if (config.useManager) add("--manager")
@ -38,6 +39,7 @@ object Patcher {
if (!MyKeyStore.useDefault) { if (!MyKeyStore.useDefault) {
addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword)) addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword))
} }
addAll(apkPaths)
}.toTypedArray() }.toTypedArray()
} }
} }
@ -56,20 +58,22 @@ object Patcher {
lspApp.targetApkFiles?.clear() lspApp.targetApkFiles?.clear()
val apkFileList = arrayListOf<File>() val apkFileList = arrayListOf<File>()
lspApp.tmpApkDir.walk() lspApp.tmpApkDir.walk()
.filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } .filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }
.forEach { apk -> .forEach { tempApkFile ->
val file = root.createFile("application/vnd.android.package-archive", apk.name) val cachedApkFile = File(lspApp.externalCacheDir, tempApkFile.name)
?: throw IOException("Failed to create output file") if (tempApkFile.renameTo(cachedApkFile).not()) {
val output = lspApp.contentResolver.openOutputStream(file.uri) tempApkFile.copyTo(cachedApkFile, overwrite = true)
?: throw IOException("Failed to open output stream") tempApkFile.delete()
val apkFile = File(lspApp.externalCacheDir, apk.name) }
apk.copyTo(apkFile, overwrite = true) apkFileList.add(cachedApkFile)
apkFileList.add(apkFile)
output.use { val finalFile = root.createFile("application/vnd.android.package-archive", cachedApkFile.name)
apk.inputStream().use { input -> ?: throw IOException("無法建立輸出檔案: ${cachedApkFile.name}")
lspApp.contentResolver.openOutputStream(finalFile.uri)?.use { output ->
cachedApkFile.inputStream().use { input ->
input.copyTo(output) input.copyTo(output)
} }
} } ?: throw IOException("Unable to open an output stream: ${finalFile.uri}")
} }
lspApp.targetApkFiles = apkFileList lspApp.targetApkFiles = apkFileList
logger.i("Patched files are saved to ${root.uri.lastPathSegment}") 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.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.DefaultLifecycleObserver 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.NavResult
import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.AnywhereDropdown
import org.lsposed.lspatch.ui.component.SelectionColumn import org.lsposed.lspatch.ui.component.SelectionColumn
import org.lsposed.lspatch.ui.component.ShimmerAnimation import org.lsposed.lspatch.ui.component.ShimmerAnimation
import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox 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.component.settings.SettingsItem
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
import org.lsposed.lspatch.ui.util.InstallResultReceiver import org.lsposed.lspatch.ui.util.InstallResultReceiver
@ -87,19 +89,20 @@ fun NewPatchScreen(
) { ) {
val viewModel = viewModel<NewPatchViewModel>() val viewModel = viewModel<NewPatchViewModel>()
val snackbarHost = LocalSnackbarHost.current val snackbarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val errorUnknown = stringResource(R.string.error_unknown) val errorUnknown = stringResource(R.string.error_unknown)
val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks -> val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks ->
if (apks.isEmpty()) { if (apks.isEmpty()) {
navigator.navigateUp() navigator.navigateUp()
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
} }
runBlocking { scope.launch {
LSPPackageManager.getAppInfoFromApks(apks) LSPPackageManager.getAppInfoFromApks(apks)
.onSuccess { .onSuccess {
viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) viewModel.dispatch(ViewAction.ConfigurePatch(it.first()))
} }
.onFailure { .onFailure {
lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown) } snackbarHost.showSnackbar(it.message ?: errorUnknown)
navigator.navigateUp() navigator.navigateUp()
} }
} }
@ -112,20 +115,16 @@ fun NewPatchScreen(
if (apks.isEmpty()) { if (apks.isEmpty()) {
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
} }
runBlocking { scope.launch {
LSPPackageManager.getAppInfoFromApks(apks).onSuccess { it -> LSPPackageManager.getAppInfoFromApks(apks).onSuccess { appInfos ->
viewModel.embeddedModules = it.filter { it.isXposedModule }.ifEmpty { val modules = appInfos.filter { it.isXposedModule }
lspApp.globalScope.launch { if (modules.isEmpty()) {
snackbarHost.showSnackbar(noXposedModules) snackbarHost.showSnackbar(noXposedModules)
} } else {
return@onSuccess viewModel.embeddedModules = modules
} }
}.onFailure { }.onFailure {
lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown)
snackbarHost.showSnackbar(
it.message ?: errorUnknown
)
}
} }
} }
} }
@ -147,16 +146,12 @@ fun NewPatchScreen(
} }
ACTION_INTENT_INSTALL -> { ACTION_INTENT_INSTALL -> {
runBlocking { data?.let { uri ->
data?.let { uri -> scope.launch {
LSPPackageManager.getAppInfoFromApks(listOf(uri)).onSuccess { LSPPackageManager.getAppInfoFromApks(listOf(uri)).onSuccess {
viewModel.dispatch(ViewAction.ConfigurePatch(it.first())) viewModel.dispatch(ViewAction.ConfigurePatch(it.first()))
}.onFailure { }.onFailure {
lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown)
snackbarHost.showSnackbar(
it.message ?: errorUnknown
)
}
navigator.navigateUp() 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( SettingsCheckBox(
modifier = Modifier modifier = Modifier
.padding(top = 6.dp) .padding(top = 6.dp)
@ -430,20 +432,19 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = shellBoxMaxHeight) .heightIn(max = shellBoxMaxHeight)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(brush) .background(MaterialTheme.colorScheme.surfaceVariant) // Replaced 'brush' with a theme color
.padding(horizontal = 24.dp, vertical = 18.dp) .padding(horizontal = 24.dp, vertical = 18.dp)
) { ) {
items(viewModel.logs) { items(viewModel.logs) {
when (it.first) { when (it.first) {
Log.DEBUG -> Text(text = it.second) Log.DEBUG, Log.INFO -> 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)
} }
} }
} }
LaunchedEffect(scrollState.lastItemIndex) { LaunchedEffect(scrollState.lastItemIndex) {
if (!scrollState.isScrolledToEnd) { if (scrollState.lastItemIndex != null && !scrollState.isScrolledToEnd) {
scrollState.animateScrollToItem(scrollState.lastItemIndex!!) scrollState.animateScrollToItem(scrollState.lastItemIndex!!)
} }
} }
@ -453,7 +454,6 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
when (viewModel.patchState) { when (viewModel.patchState) {
PatchState.PATCHING -> BackHandler {} PatchState.PATCHING -> BackHandler {}
PatchState.FINISHED -> { PatchState.FINISHED -> {
val shizukuUnavailable = stringResource(R.string.shizuku_unavailable)
val installSuccessfully = stringResource(R.string.patch_install_successfully) val installSuccessfully = stringResource(R.string.patch_install_successfully)
val installFailed = stringResource(R.string.patch_install_failed) val installFailed = stringResource(R.string.patch_install_failed)
val copyError = stringResource(R.string.copy_error) 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 -> val onFinish: (Int, String?) -> Unit = { status, message ->
scope.launch { scope.launch {
if (status == PackageInstaller.STATUS_SUCCESS) { if (status == PackageInstaller.STATUS_SUCCESS) {
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } snackbarHost.showSnackbar(installSuccessfully)
navigator.navigateUp() navigator.navigateUp()
} else if (status != LSPPackageManager.STATUS_USER_CANCELLED) { } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) {
val result = snackbarHost.showSnackbar(installFailed, copyError) val result = snackbarHost.showSnackbar(installFailed, copyError)
@ -470,6 +470,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message)) cm.setPrimaryClip(ClipData.newPlainText("LSPatch", message))
} }
} }
installation = null // Reset installation state
} }
} }
when (installation) { when (installation) {
@ -506,7 +507,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { onClick = {
val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 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)) } 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 @Composable
private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
val scope = rememberCoroutineScope() 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) } var installing by remember { mutableStateOf(0) } // 0: idle, 1: installing, 2: uninstalling
suspend fun doInstall() { suspend fun doInstall() {
Log.i(TAG, "Installing app ${patchApp.app.packageName}") Log.i(TAG, "Installing app ${patchApp.app.packageName}")
installing = 1 installing = 1
@ -532,49 +564,29 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
onFinish(status, message) onFinish(status, message)
} }
LaunchedEffect(Unit) { LaunchedEffect(uninstallFirst) {
if (!uninstallFirst) { if (!uninstallFirst && installing == 0) {
doInstall() doInstall()
} }
} }
if (uninstallFirst) { if (uninstallFirst) {
AlertDialog( UninstallConfirmationDialog(
onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") }, onDismiss = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
confirmButton = { onConfirm = {
TextButton( scope.launch {
onClick = { Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
scope.launch { installing = 2
Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName)
uninstallFirst = false installing = 0
installing = 2 Log.i(TAG, "Uninstallation end: $status, $message")
val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName) if (status == PackageInstaller.STATUS_SUCCESS) {
installing = 0 uninstallFirst = false // This will trigger the LaunchedEffect to install
Log.i(TAG, "Uninstallation end: $status, $message") } else {
if (status == PackageInstaller.STATUS_SUCCESS) { onFinish(status, message)
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)) }
) )
} }
@ -589,6 +601,15 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
fontFamily = FontFamily.Serif, fontFamily = FontFamily.Serif,
textAlign = TextAlign.Center 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 @Composable
private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var uninstallFirst by remember { var uninstallFirst by remember { mutableStateOf(checkIsApkFixedByLSP(lspApp, patchApp.app.packageName)) }
mutableStateOf(
checkIsApkFixedByLSP(
lspApp,
patchApp.app.packageName
)
)
}
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current val context = LocalContext.current
val splitInstallReceiver by lazy { InstallResultReceiver() } val lifecycleOwner = LocalLifecycleOwner.current
val splitInstallReceiver = remember { InstallResultReceiver() }
fun doInstall() { 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 val apkFiles = lspApp.targetApkFiles
if (apkFiles.isNullOrEmpty()){ if (apkFiles.isNullOrEmpty()){
onFinish(PackageInstaller.STATUS_FAILURE, "No target APK files found for installation") 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) { if (apkFiles.size > 1) {
scope.launch { scope.launch {
val success = installApks(lspApp, apkFiles) val success = installApks(lspApp, apkFiles)
if (success) { onFinish(
onFinish( if (success) PackageInstaller.STATUS_SUCCESS else PackageInstaller.STATUS_FAILURE,
PackageInstaller.STATUS_SUCCESS, if (success) "Split APKs installed successfully" else "Failed to install split APKs"
"Split APKs installed successfully" )
)
} else {
onFinish(
PackageInstaller.STATUS_FAILURE,
"Failed to install split APKs"
)
}
} }
} else { } else {
installApk(lspApp, apkFiles.first()) 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) { DisposableEffect(lifecycleOwner, context) {
val observer = object : DefaultLifecycleObserver { val intentFilter = IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS)
@SuppressLint("UnspecifiedRegisterReceiverFlag") // Correctly handle receiver registration for different Android versions
override fun onCreate(owner: LifecycleOwner) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.registerReceiver(splitInstallReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED) } else {
} else { context.registerReceiver(splitInstallReceiver, intentFilter)
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()
}
}
} }
lifecycleOwner.lifecycle.addObserver(observer) onDispose {
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } 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) { if (uninstallFirst) {
AlertDialog( UninstallConfirmationDialog(
onDismissRequest = { onDismiss = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
onFinish( onConfirm = {
LSPPackageManager.STATUS_USER_CANCELLED, scope.launch {
"User cancelled" Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
) uninstallApkByPackageName(lspApp, patchApp.app.packageName)
}, // After uninstall intent is sent, we can assume it will proceed.
confirmButton = { uninstallFirst = false
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)) }
) )
} }
} }

View File

@ -38,7 +38,9 @@ class NewPatchViewModel : ViewModel() {
var patchState by mutableStateOf(PatchState.INIT) var patchState by mutableStateOf(PatchState.INIT)
private set private set
// Patch Configuration
var useManager by mutableStateOf(true) var useManager by mutableStateOf(true)
var newPackageName by mutableStateOf("")
var debuggable by mutableStateOf(false) var debuggable by mutableStateOf(false)
var overrideVersionCode by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false)
var sigBypassLevel by mutableStateOf(2) var sigBypassLevel by mutableStateOf(2)
@ -90,14 +92,17 @@ class NewPatchViewModel : ViewModel() {
Log.d(TAG, "Configuring patch for ${app.app.packageName}") Log.d(TAG, "Configuring patch for ${app.app.packageName}")
patchApp = app patchApp = app
patchState = PatchState.CONFIGURING patchState = PatchState.CONFIGURING
newPackageName = app.app.packageName
} }
private fun submitPatch() { private fun submitPatch() {
Log.d(TAG, "Submit patch") Log.d(TAG, "Submit patch")
if (useManager) embeddedModules = emptyList() if (useManager) embeddedModules = emptyList()
val config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, outputLog, newPackageName)
patchOptions = Patcher.Options( patchOptions = Patcher.Options(
newPackageName = newPackageName,
injectDex = injectDex, injectDex = injectDex,
config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, outputLog), config = config,
apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()), apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()),
embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) }
) )
@ -117,4 +122,4 @@ class NewPatchViewModel : ViewModel() {
LSPPackageManager.cleanTmpApkDir() LSPPackageManager.cleanTmpApkDir()
} }
} }
} }

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) { if (!ShizukuApi.isPermissionGranted) {
val apkFiles = lspApp.targetApkFiles val apkFiles = lspApp.targetApkFiles
if (apkFiles.isNullOrEmpty()){ if (apkFiles.isNullOrEmpty()){
@ -154,4 +154,4 @@ class AppManageViewModel : ViewModel() {
} }
optimizeState = ProcessingState.Done(result) optimizeState = ProcessingState.Done(result)
} }
} }

View File

@ -63,6 +63,8 @@
<string name="patch_sigbypasslv2">lv2: 绕过 PM + openat (libc)</string> <string name="patch_sigbypasslv2">lv2: 绕过 PM + openat (libc)</string>
<string name="patch_override_version_code">覆写版本号</string> <string name="patch_override_version_code">覆写版本号</string>
<string name="patch_override_version_code_desc">将修补的 App 版本号重写为 1\n这将允许后续降级安装并且通常来说这不会影响应用实际感知到的版本号</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">注入加载器 Dex</string>
<string name="patch_inject_dex_desc">对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行</string> <string name="patch_inject_dex_desc">对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行</string>
<string name="patch_output_log_to_media">日志输出到 Media 目录</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_sigbypasslv2">lv2: 繞過 PM + openat (libc)</string>
<string name="patch_override_version_code">覆蓋版本編號</string> <string name="patch_override_version_code">覆蓋版本編號</string>
<string name="patch_override_version_code_desc">將打包應用程式的版本編號改成 1\n允許以後降級安裝一般來說這不會影響應用程式實際感知的版本編號。</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">注入載入器 Dex</string>
<string name="patch_inject_dex_desc">對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行</string> <string name="patch_inject_dex_desc">對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行</string>
<string name="patch_output_log_to_media">日誌輸出到 Media 目錄</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_sigbypasslv0">lv0: Off</string>
<string name="patch_sigbypasslv1">lv1: Bypass PM</string> <string name="patch_sigbypasslv1">lv1: Bypass PM</string>
<string name="patch_sigbypasslv2">lv2: Bypass PM + openat (libc)</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">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_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> <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)); BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
config = new Gson().fromJson(streamReader, PatchConfig.class); config = new Gson().fromJson(streamReader, PatchConfig.class);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Failed to load config file"); Log.e(TAG, "Failed to load config file", e);
return null; return null;
} }
Log.i(TAG, "Use manager: " + config.useManager); 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.google.gson.Gson;
import com.wind.meditor.core.ManifestEditor; import com.wind.meditor.core.ManifestEditor;
import com.wind.meditor.property.AttributeItem; import com.wind.meditor.property.AttributeItem;
import com.wind.meditor.property.AttributeMapper;
import com.wind.meditor.property.ModificationProperty; import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.property.PermissionMapper;
import com.wind.meditor.utils.NodeValue; 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.apache.commons.io.FilenameUtils;
import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.Constants;
@ -43,9 +47,12 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class LSPatch { public class LSPatch {
@ -99,6 +106,8 @@ public class LSPatch {
@Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk") @Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk")
private List<String> modules = new ArrayList<>(); 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 String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
private static final HashSet<String> ARCHES = new HashSet<>(Arrays.asList( private static final HashSet<String> ARCHES = new HashSet<>(Arrays.asList(
@ -226,17 +235,32 @@ public class LSPatch {
if (manifestEntry == null) if (manifestEntry == null)
throw new PatchError("Provided file is not a valid apk"); throw new PatchError("Provided file is not a valid apk");
String newPackage = newPackageName;
// parse the app appComponentFactory full name from the manifest file // parse the app appComponentFactory full name from the manifest file
final String appComponentFactory; final String appComponentFactory;
int minSdkVersion; int minSdkVersion;
ManifestParser.Pair pair;
try (var is = manifestEntry.open()) { try (var is = manifestEntry.open()) {
var pair = ManifestParser.parseManifestFile(is); pair = ManifestParser.parseManifestFile(is);
if (pair == null) if (pair == null)
throw new PatchError("Failed to parse AndroidManifest.xml"); throw new PatchError("Failed to parse AndroidManifest.xml");
appComponentFactory = pair.appComponentFactory; appComponentFactory = pair.appComponentFactory;
minSdkVersion = pair.minSdkVersion; minSdkVersion = pair.minSdkVersion;
logger.d("original appComponentFactory class: " + appComponentFactory); logger.d("original appComponentFactory class: " + appComponentFactory);
logger.d("original minSdkVersion: " + minSdkVersion); 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; final boolean skipSplit = apkPaths.size() > 1 && srcApkFile.getName().startsWith("split_") && appComponentFactory == null;
@ -254,10 +278,10 @@ public class LSPatch {
logger.i("Patching apk..."); logger.i("Patching apk...");
// modify manifest // 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 configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8);
final var metadata = Base64.getEncoder().encodeToString(configBytes); 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); dstZFile.add(ANDROID_MANIFEST_XML, is);
} catch (Throwable e) { } catch (Throwable e) {
throw new PatchError("Error when modifying manifest", e); throw new PatchError("Error when modifying manifest", e);
@ -331,6 +355,57 @@ public class LSPatch {
logger.i("Done. Output APK: " + outputFile.getAbsolutePath()); 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) { private void embedModules(ZFile zFile) {
for (var module : modules) { for (var module : modules) {
File file = new File(module); File file = new File(module);
@ -343,12 +418,12 @@ public class LSPatch {
logger.i(" - " + packageName); logger.i(" - " + packageName);
zFile.add(EMBEDDED_MODULES_ASSET_PATH + packageName + ".apk", fileIs); zFile.add(EMBEDDED_MODULES_ASSET_PATH + packageName + ".apk", fileIs);
} catch (NullPointerException | IOException e) { } 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(); ModificationProperty property = new ModificationProperty();
if (overrideVersionCode) if (overrideVersionCode)
@ -357,6 +432,34 @@ public class LSPatch {
property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, 27)); property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, 27));
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag));
property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); 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)); property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata));
// TODO: replace query_all with queries -> manager // TODO: replace query_all with queries -> manager
if (useManager) if (useManager)

View File

@ -6,6 +6,8 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import pxb.android.axml.AxmlParser; import pxb.android.axml.AxmlParser;
@ -19,6 +21,9 @@ public class ManifestParser {
String packageName = null; String packageName = null;
String appComponentFactory = null; String appComponentFactory = null;
int minSdkVersion = 0; int minSdkVersion = 0;
List<String> permissions = new ArrayList<>();
List<String> use_permissions = new ArrayList<>();
List<String> authorities = new ArrayList<>();
try { try {
while (true) { 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) { if ("appComponentFactory".equals(attrName) || attrNameRes == 0x0101057a) {
appComponentFactory = parser.getAttrValue(i).toString(); appComponentFactory = parser.getAttrValue(i).toString();
} }
if (packageName != null && packageName.length() > 0 && // if (packageName != null && packageName.length() > 0 &&
appComponentFactory != null && appComponentFactory.length() > 0 && // appComponentFactory != null && appComponentFactory.length() > 0 &&
minSdkVersion > 0 // minSdkVersion > 0
) { // ) {
return new Pair(packageName, appComponentFactory, minSdkVersion); // return new Pair(packageName, appComponentFactory, minSdkVersion);
} // }
} }
} else if (type == AxmlParser.END_TAG) { } else if (type == AxmlParser.END_TAG) {
// ignored // ignored
@ -65,7 +95,11 @@ public class ManifestParser {
return null; 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 String appComponentFactory;
public int minSdkVersion; public int minSdkVersion;
public List<String> permissions;
public List<String> use_permissions;
public List<String> authorities;
public Pair(String packageName, String appComponentFactory, int minSdkVersion) { public Pair(String packageName, String appComponentFactory, int minSdkVersion) {
this.packageName = packageName; this.packageName = packageName;
this.appComponentFactory = appComponentFactory; this.appComponentFactory = appComponentFactory;
this.minSdkVersion = minSdkVersion; 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( include(
":apache", ":apache",
":apkzlib", ":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:manager-service").projectDir = file("core/services/manager-service")
project(":services:xposed-service:interface").projectDir = file("core/services/xposed-service/interface") project(":services:xposed-service:interface").projectDir = file("core/services/xposed-service/interface")
buildCache { local { removeUnusedEntriesAfterDays = 1 } } buildCache { local { removeUnusedEntriesAfterDays = 1 } }

View File

@ -11,6 +11,7 @@ public class PatchConfig {
public final String appComponentFactory; public final String appComponentFactory;
public final LSPConfig lspConfig; public final LSPConfig lspConfig;
public final String managerPackageName; public final String managerPackageName;
public final String newPackage;
public PatchConfig( public PatchConfig(
boolean useManager, boolean useManager,
@ -19,7 +20,8 @@ public class PatchConfig {
int sigBypassLevel, int sigBypassLevel,
String originalSignature, String originalSignature,
String appComponentFactory, String appComponentFactory,
boolean outputLog boolean outputLog,
String newPackage
) { ) {
this.useManager = useManager; this.useManager = useManager;
this.debuggable = debuggable; this.debuggable = debuggable;
@ -29,6 +31,7 @@ public class PatchConfig {
this.appComponentFactory = appComponentFactory; this.appComponentFactory = appComponentFactory;
this.lspConfig = LSPConfig.instance; this.lspConfig = LSPConfig.instance;
this.managerPackageName = Constants.MANAGER_PACKAGE_NAME; this.managerPackageName = Constants.MANAGER_PACKAGE_NAME;
this.newPackage = newPackage;
this.outputLog = outputLog; this.outputLog = outputLog;
} }
} }