Skip to content

Commit 45a9f69

Browse files
NkBexihan123
authored andcommitted
feat: Can install patched APKs via system API
https: //github.com/JingMatrix/LSPatch/pull/46 Co-Authored-By: xihan123 <srxqzxs@vip.qq.com>
1 parent b1439dc commit 45a9f69

11 files changed

Lines changed: 353 additions & 31 deletions

File tree

manager/src/main/AndroidManifest.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@
4949
</intent-filter>
5050
</activity-alias>
5151

52-
52+
<receiver
53+
android:name=".ui.util.InstallResultReceiver"
54+
android:exported="true">
55+
<intent-filter>
56+
<action android:name="${applicationId}.INSTALL_STATUS" />
57+
</intent-filter>
58+
</receiver>
59+
<action android:name="${applicationId}.INSTALL_STATUS" />
5360
<service
5461
android:name=".manager.ModuleService"
5562
android:exported="true"
@@ -63,6 +70,15 @@
6370
android:exported="true"
6471
android:multiprocess="false"
6572
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
73+
<provider
74+
android:name="androidx.core.content.FileProvider"
75+
android:authorities="${applicationId}.fileprovider"
76+
android:exported="false"
77+
android:grantUriPermissions="true">
78+
<meta-data
79+
android:name="android.support.FILE_PROVIDER_PATHS"
80+
android:resource="@drawable/file_paths" />
81+
</provider>
6682
</application>
6783

6884
</manifest>

manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class LSPApplication : Application() {
1818

1919
lateinit var prefs: SharedPreferences
2020
lateinit var tmpApkDir: File
21-
21+
var targetApkFiles: ArrayList<File>? = null
2222
val globalScope = CoroutineScope(Dispatchers.Default)
2323

2424
override fun onCreate() {

manager/src/main/java/org/lsposed/lspatch/Patcher.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.lsposed.lspatch.share.Constants
1010
import org.lsposed.lspatch.share.PatchConfig
1111
import org.lsposed.patch.LSPatch
1212
import org.lsposed.patch.util.Logger
13+
import java.io.File
1314
import java.io.IOException
1415
import java.util.Collections.addAll
1516

@@ -52,20 +53,26 @@ object Patcher {
5253
root.listFiles().forEach {
5354
if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete()
5455
}
56+
lspApp.targetApkFiles?.clear()
57+
val apkFileList = arrayListOf<File>()
5558
lspApp.tmpApkDir.walk()
5659
.filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }
5760
.forEach { apk ->
5861
val file = root.createFile("application/vnd.android.package-archive", apk.name)
5962
?: throw IOException("Failed to create output file")
6063
val output = lspApp.contentResolver.openOutputStream(file.uri)
6164
?: throw IOException("Failed to open output stream")
65+
val apkFile = File(lspApp.externalCacheDir, apk.name)
66+
apk.copyTo(apkFile, overwrite = true)
67+
apkFileList.add(apkFile)
6268
output.use {
6369
apk.inputStream().use { input ->
6470
input.copyTo(output)
6571
}
6672
}
6773
}
74+
lspApp.targetApkFiles = apkFileList
6875
logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
6976
}
7077
}
71-
}
78+
}

manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package org.lsposed.lspatch.ui.page
22

3+
import android.annotation.SuppressLint
34
import android.content.ClipData
45
import android.content.ClipboardManager
56
import android.content.Context
7+
import android.content.Context.RECEIVER_NOT_EXPORTED
8+
import android.content.IntentFilter
69
import android.content.pm.PackageInstaller
710
import android.net.Uri
11+
import android.os.Build
812
import android.util.Log
913
import androidx.activity.compose.BackHandler
1014
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -28,10 +32,14 @@ import androidx.compose.runtime.*
2832
import androidx.compose.ui.Alignment
2933
import androidx.compose.ui.Modifier
3034
import androidx.compose.ui.draw.clip
35+
import androidx.compose.ui.platform.LocalContext
3136
import androidx.compose.ui.res.stringResource
3237
import androidx.compose.ui.text.font.FontFamily
3338
import androidx.compose.ui.text.style.TextAlign
3439
import androidx.compose.ui.unit.dp
40+
import androidx.lifecycle.DefaultLifecycleObserver
41+
import androidx.lifecycle.LifecycleOwner
42+
import androidx.lifecycle.compose.LocalLifecycleOwner
3543
import androidx.lifecycle.viewmodel.compose.viewModel
3644
import com.ramcosta.composedestinations.annotation.Destination
3745
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -47,9 +55,14 @@ import org.lsposed.lspatch.ui.component.ShimmerAnimation
4755
import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
4856
import org.lsposed.lspatch.ui.component.settings.SettingsItem
4957
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
58+
import org.lsposed.lspatch.ui.util.InstallResultReceiver
5059
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
60+
import org.lsposed.lspatch.ui.util.checkIsApkFixedByLSP
61+
import org.lsposed.lspatch.ui.util.installApk
62+
import org.lsposed.lspatch.ui.util.installApks
5163
import org.lsposed.lspatch.ui.util.isScrolledToEnd
5264
import org.lsposed.lspatch.ui.util.lastItemIndex
65+
import org.lsposed.lspatch.ui.util.uninstallApkByPackageName
5366
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
5467
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
5568
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
@@ -377,6 +390,7 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
377390
}
378391
}
379392

393+
@SuppressLint("UnusedBoxWithConstraintsScope")
380394
@Composable
381395
private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
382396
val viewModel = viewModel<NewPatchViewModel>()
@@ -435,10 +449,9 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
435449
val installSuccessfully = stringResource(R.string.patch_install_successfully)
436450
val installFailed = stringResource(R.string.patch_install_failed)
437451
val copyError = stringResource(R.string.copy_error)
438-
var installing by remember { mutableStateOf(false) }
439-
if (installing) InstallDialog(viewModel.patchApp) { status, message ->
452+
var installation by remember { mutableStateOf<NewPatchViewModel.InstallMethod?>(null) }
453+
val onFinish: (Int, String?) -> Unit = { status, message ->
440454
scope.launch {
441-
installing = false
442455
if (status == PackageInstaller.STATUS_SUCCESS) {
443456
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
444457
navigator.navigateUp()
@@ -451,6 +464,11 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
451464
}
452465
}
453466
}
467+
when (installation) {
468+
NewPatchViewModel.InstallMethod.SYSTEM -> InstallDialog2(viewModel.patchApp, onFinish)
469+
NewPatchViewModel.InstallMethod.SHIZUKU -> InstallDialog(viewModel.patchApp, onFinish)
470+
null -> {}
471+
}
454472
Row(Modifier.padding(top = 12.dp)) {
455473
Button(
456474
modifier = Modifier.weight(1f),
@@ -461,13 +479,8 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
461479
Button(
462480
modifier = Modifier.weight(1f),
463481
onClick = {
464-
if (!ShizukuApi.isPermissionGranted) {
465-
scope.launch {
466-
snackbarHost.showSnackbar(shizukuUnavailable)
467-
}
468-
} else {
469-
installing = true
470-
}
482+
installation = if (!ShizukuApi.isPermissionGranted) NewPatchViewModel.InstallMethod.SYSTEM else NewPatchViewModel.InstallMethod.SHIZUKU
483+
Log.d(TAG, "Installation method: $installation")
471484
},
472485
content = { Text(stringResource(R.string.install)) }
473486
)
@@ -572,3 +585,116 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
572585
)
573586
}
574587
}
588+
589+
@Composable
590+
private fun InstallDialog2(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
591+
val scope = rememberCoroutineScope()
592+
var uninstallFirst by remember {
593+
mutableStateOf(
594+
checkIsApkFixedByLSP(
595+
lspApp,
596+
patchApp.app.packageName
597+
)
598+
)
599+
}
600+
val lifecycleOwner = LocalLifecycleOwner.current
601+
val context = LocalContext.current
602+
val splitInstallReceiver by lazy { InstallResultReceiver() }
603+
fun doInstall() {
604+
Log.i(TAG, "Installing app ${patchApp.app.packageName}")
605+
val apkFiles = lspApp.targetApkFiles
606+
if (apkFiles.isNullOrEmpty()){
607+
onFinish(PackageInstaller.STATUS_FAILURE, "No target APK files found for installation")
608+
return
609+
}
610+
if (apkFiles.size > 1) {
611+
scope.launch {
612+
val success = installApks(lspApp, apkFiles)
613+
if (success) {
614+
onFinish(
615+
PackageInstaller.STATUS_SUCCESS,
616+
"Split APKs installed successfully"
617+
)
618+
} else {
619+
onFinish(
620+
PackageInstaller.STATUS_FAILURE,
621+
"Failed to install split APKs"
622+
)
623+
}
624+
}
625+
} else {
626+
installApk(lspApp, apkFiles.first())
627+
}
628+
}
629+
630+
DisposableEffect(lifecycleOwner) {
631+
val observer = object : DefaultLifecycleObserver {
632+
@SuppressLint("UnspecifiedRegisterReceiverFlag")
633+
override fun onCreate(owner: LifecycleOwner) {
634+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
635+
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED)
636+
} else {
637+
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS))
638+
}
639+
}
640+
641+
override fun onDestroy(owner: LifecycleOwner) {
642+
context.unregisterReceiver(splitInstallReceiver)
643+
}
644+
645+
override fun onResume(owner: LifecycleOwner) {
646+
if (!uninstallFirst) {
647+
Log.d(TAG,"Starting installation without uninstalling first")
648+
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled")
649+
doInstall()
650+
}
651+
}
652+
}
653+
654+
lifecycleOwner.lifecycle.addObserver(observer)
655+
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
656+
}
657+
658+
if (uninstallFirst) {
659+
AlertDialog(
660+
onDismissRequest = {
661+
onFinish(
662+
LSPPackageManager.STATUS_USER_CANCELLED,
663+
"User cancelled"
664+
)
665+
},
666+
confirmButton = {
667+
TextButton(
668+
onClick = {
669+
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Reset")
670+
scope.launch {
671+
Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
672+
uninstallApkByPackageName(lspApp, patchApp.app.packageName)
673+
uninstallFirst = false
674+
}
675+
},
676+
content = { Text(stringResource(android.R.string.ok)) }
677+
)
678+
},
679+
dismissButton = {
680+
TextButton(
681+
onClick = {
682+
onFinish(
683+
LSPPackageManager.STATUS_USER_CANCELLED,
684+
"User cancelled"
685+
)
686+
},
687+
content = { Text(stringResource(android.R.string.cancel)) }
688+
)
689+
},
690+
title = {
691+
Text(
692+
modifier = Modifier.fillMaxWidth(),
693+
text = stringResource(R.string.uninstall),
694+
textAlign = TextAlign.Center
695+
)
696+
},
697+
text = { Text(stringResource(R.string.patch_uninstall_text)) }
698+
)
699+
}
700+
}

manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,7 @@ fun AppManageBody(
195195
onClick = {
196196
expanded = false
197197
scope.launch {
198-
if (!ShizukuApi.isPermissionGranted) {
199-
snackbarHost.showSnackbar(shizukuUnavailable)
200-
} else {
201-
viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
202-
}
198+
viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
203199
}
204200
}
205201
)

0 commit comments

Comments
 (0)