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:
parent
9a58d53314
commit
d644c22ade
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)) }
|
||||||
)
|
)
|
||||||
|
|
@ -519,51 +520,21 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
|
private fun UninstallConfirmationDialog(
|
||||||
val scope = rememberCoroutineScope()
|
onDismiss: () -> Unit,
|
||||||
var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }
|
onConfirm: () -> Unit
|
||||||
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) {
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
|
onDismissRequest = onDismiss,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = onConfirm,
|
||||||
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)) }
|
content = { Text(stringResource(android.R.string.ok)) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
|
onClick = onDismiss,
|
||||||
content = { Text(stringResource(android.R.string.cancel)) }
|
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) {
|
if (installing != 0) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
|
|
@ -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(
|
||||||
PackageInstaller.STATUS_SUCCESS,
|
if (success) PackageInstaller.STATUS_SUCCESS else PackageInstaller.STATUS_FAILURE,
|
||||||
"Split APKs installed successfully"
|
if (success) "Split APKs installed successfully" else "Failed to install split APKs"
|
||||||
)
|
)
|
||||||
} 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(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED)
|
context.registerReceiver(splitInstallReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
|
||||||
} else {
|
} else {
|
||||||
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS))
|
context.registerReceiver(splitInstallReceiver, intentFilter)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
onDispose {
|
||||||
context.unregisterReceiver(splitInstallReceiver)
|
context.unregisterReceiver(splitInstallReceiver)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
LaunchedEffect(uninstallFirst) {
|
||||||
if (!uninstallFirst) {
|
if (!uninstallFirst) {
|
||||||
Log.d(TAG,"Starting installation without uninstalling first")
|
Log.d(TAG, "State changed to install, starting installation via system.")
|
||||||
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled")
|
|
||||||
doInstall()
|
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) {
|
if (uninstallFirst) {
|
||||||
AlertDialog(
|
UninstallConfirmationDialog(
|
||||||
onDismissRequest = {
|
onDismiss = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled") },
|
||||||
onFinish(
|
onConfirm = {
|
||||||
LSPPackageManager.STATUS_USER_CANCELLED,
|
|
||||||
"User cancelled"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Reset")
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
|
Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
|
||||||
uninstallApkByPackageName(lspApp, patchApp.app.packageName)
|
uninstallApkByPackageName(lspApp, patchApp.app.packageName)
|
||||||
|
// After uninstall intent is sent, we can assume it will proceed.
|
||||||
uninstallFirst = false
|
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)) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()){
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +29,7 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "LSPatch"
|
rootProject.name = "NPatch"
|
||||||
include(
|
include(
|
||||||
":apache",
|
":apache",
|
||||||
":apkzlib",
|
":apkzlib",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue