11package org.lsposed.lspatch.ui.page
22
3+ import android.annotation.SuppressLint
34import android.content.ClipData
45import android.content.ClipboardManager
56import android.content.Context
7+ import android.content.Context.RECEIVER_NOT_EXPORTED
8+ import android.content.IntentFilter
69import android.content.pm.PackageInstaller
710import android.net.Uri
11+ import android.os.Build
812import android.util.Log
913import androidx.activity.compose.BackHandler
1014import androidx.activity.compose.rememberLauncherForActivityResult
@@ -28,10 +32,14 @@ import androidx.compose.runtime.*
2832import androidx.compose.ui.Alignment
2933import androidx.compose.ui.Modifier
3034import androidx.compose.ui.draw.clip
35+ import androidx.compose.ui.platform.LocalContext
3136import androidx.compose.ui.res.stringResource
3237import androidx.compose.ui.text.font.FontFamily
3338import androidx.compose.ui.text.style.TextAlign
3439import androidx.compose.ui.unit.dp
40+ import androidx.lifecycle.DefaultLifecycleObserver
41+ import androidx.lifecycle.LifecycleOwner
42+ import androidx.lifecycle.compose.LocalLifecycleOwner
3543import androidx.lifecycle.viewmodel.compose.viewModel
3644import com.ramcosta.composedestinations.annotation.Destination
3745import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -47,9 +55,14 @@ import org.lsposed.lspatch.ui.component.ShimmerAnimation
4755import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
4856import org.lsposed.lspatch.ui.component.settings.SettingsItem
4957import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
58+ import org.lsposed.lspatch.ui.util.InstallResultReceiver
5059import 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
5163import org.lsposed.lspatch.ui.util.isScrolledToEnd
5264import org.lsposed.lspatch.ui.util.lastItemIndex
65+ import org.lsposed.lspatch.ui.util.uninstallApkByPackageName
5366import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
5467import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
5568import 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
381395private 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+ }
0 commit comments