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