Enhance installation and cleanup processes
Co-Authored-By: xihan123 <srxqzxs@vip.qq.com>
This commit is contained in:
parent
c0aecf9e33
commit
0d55e2e602
|
|
@ -1,16 +1,15 @@
|
||||||
[versions]
|
[versions]
|
||||||
room = "2.7.2"
|
room = "2.7.2"
|
||||||
accompanist = "0.36.0"
|
accompanist = "0.36.0"
|
||||||
compose-destinations = "1.11.8"
|
compose-destinations = "1.11.7"
|
||||||
shizuku = "13.1.5"
|
shizuku = "13.1.5"
|
||||||
hiddenapi = "4.4.0"
|
hiddenapi = "4.4.0"
|
||||||
compose-bom = "2025.08.01"
|
compose-bom = "2025.08.00"
|
||||||
kotlin = "2.2.10"
|
kotlin = "2.2.10"
|
||||||
ksp = "2.2.10-2.0.2"
|
ksp = "2.2.10-2.0.2"
|
||||||
commons-io = "2.20.0"
|
commons-io = "2.20.0"
|
||||||
beust-jcommander = "1.82"
|
beust-jcommander = "1.82"
|
||||||
google-gson = "2.13.1"
|
google-gson = "2.13.1"
|
||||||
material = "1.12.0"
|
|
||||||
core-ktx = "1.16.0"
|
core-ktx = "1.16.0"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,14 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".ui.util.InstallResultReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${applicationId}.INSTALL_STATUS" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".manager.ModuleService"
|
android:name=".manager.ModuleService"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class LSPApplication : Application() {
|
||||||
lateinit var prefs: SharedPreferences
|
lateinit var prefs: SharedPreferences
|
||||||
lateinit var tmpApkDir: File
|
lateinit var tmpApkDir: File
|
||||||
|
|
||||||
|
var targetApkFiles: ArrayList<File>? = null
|
||||||
val globalScope = CoroutineScope(Dispatchers.Default)
|
val globalScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import org.lsposed.lspatch.share.Constants
|
||||||
import org.lsposed.lspatch.share.PatchConfig
|
import org.lsposed.lspatch.share.PatchConfig
|
||||||
import org.lsposed.patch.LSPatch
|
import org.lsposed.patch.LSPatch
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Collections.addAll
|
import java.util.Collections.addAll
|
||||||
|
|
||||||
|
|
@ -52,6 +53,8 @@ object Patcher {
|
||||||
root.listFiles().forEach {
|
root.listFiles().forEach {
|
||||||
if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete()
|
if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete()
|
||||||
}
|
}
|
||||||
|
lspApp.targetApkFiles?.clear()
|
||||||
|
val apkFileList = arrayListOf<File>()
|
||||||
lspApp.tmpApkDir.walk()
|
lspApp.tmpApkDir.walk()
|
||||||
.filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }
|
.filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }
|
||||||
.forEach { apk ->
|
.forEach { apk ->
|
||||||
|
|
@ -59,12 +62,16 @@ object Patcher {
|
||||||
?: throw IOException("Failed to create output file")
|
?: throw IOException("Failed to create output file")
|
||||||
val output = lspApp.contentResolver.openOutputStream(file.uri)
|
val output = lspApp.contentResolver.openOutputStream(file.uri)
|
||||||
?: throw IOException("Failed to open output stream")
|
?: throw IOException("Failed to open output stream")
|
||||||
|
val apkFile = File(lspApp.externalCacheDir, apk.name)
|
||||||
|
apk.copyTo(apkFile, overwrite = true)
|
||||||
|
apkFileList.add(apkFile)
|
||||||
output.use {
|
output.use {
|
||||||
apk.inputStream().use { input ->
|
apk.inputStream().use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lspApp.targetApkFiles = apkFileList
|
||||||
logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
|
logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
package org.lsposed.lspatch.ui.page
|
package org.lsposed.lspatch.ui.page
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Context.RECEIVER_NOT_EXPORTED
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
|
@ -28,10 +32,14 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
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.LifecycleOwner
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
|
@ -47,9 +55,14 @@ 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.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.LocalSnackbarHost
|
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
|
||||||
|
import org.lsposed.lspatch.ui.util.checkIsApkFixedByLSP
|
||||||
|
import org.lsposed.lspatch.ui.util.installApk
|
||||||
|
import org.lsposed.lspatch.ui.util.installApks
|
||||||
import org.lsposed.lspatch.ui.util.isScrolledToEnd
|
import org.lsposed.lspatch.ui.util.isScrolledToEnd
|
||||||
import org.lsposed.lspatch.ui.util.lastItemIndex
|
import org.lsposed.lspatch.ui.util.lastItemIndex
|
||||||
|
import org.lsposed.lspatch.ui.util.uninstallApkByPackageName
|
||||||
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
|
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
|
||||||
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
|
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
|
||||||
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
|
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
|
||||||
|
|
@ -377,6 +390,7 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
val viewModel = viewModel<NewPatchViewModel>()
|
val viewModel = viewModel<NewPatchViewModel>()
|
||||||
|
|
@ -435,10 +449,9 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
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)
|
||||||
var installing by remember { mutableStateOf(false) }
|
var installation by remember { mutableStateOf<NewPatchViewModel.InstallMethod?>(null) }
|
||||||
if (installing) InstallDialog(viewModel.patchApp) { status, message ->
|
val onFinish: (Int, String?) -> Unit = { status, message ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
installing = false
|
|
||||||
if (status == PackageInstaller.STATUS_SUCCESS) {
|
if (status == PackageInstaller.STATUS_SUCCESS) {
|
||||||
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
|
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
|
||||||
navigator.navigateUp()
|
navigator.navigateUp()
|
||||||
|
|
@ -451,6 +464,11 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
when (installation) {
|
||||||
|
NewPatchViewModel.InstallMethod.SYSTEM -> InstallDialog2(viewModel.patchApp, onFinish)
|
||||||
|
NewPatchViewModel.InstallMethod.SHIZUKU -> InstallDialog(viewModel.patchApp, onFinish)
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
Row(Modifier.padding(top = 12.dp)) {
|
Row(Modifier.padding(top = 12.dp)) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
|
@ -468,6 +486,8 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
} else {
|
} else {
|
||||||
installing = true
|
installing = true
|
||||||
}
|
}
|
||||||
|
installation = if (!ShizukuApi.isPermissionGranted) NewPatchViewModel.InstallMethod.SYSTEM else NewPatchViewModel.InstallMethod.SHIZUKU
|
||||||
|
Log.d(TAG, "Installation method: $installation")
|
||||||
},
|
},
|
||||||
content = { Text(stringResource(R.string.install)) }
|
content = { Text(stringResource(R.string.install)) }
|
||||||
)
|
)
|
||||||
|
|
@ -572,3 +592,116 @@ 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
|
||||||
|
val context = LocalContext.current
|
||||||
|
val splitInstallReceiver by lazy { InstallResultReceiver() }
|
||||||
|
fun doInstall() {
|
||||||
|
Log.i(TAG, "Installing app ${patchApp.app.packageName}")
|
||||||
|
val apkFiles = lspApp.targetApkFiles
|
||||||
|
if (apkFiles.isNullOrEmpty()){
|
||||||
|
onFinish(PackageInstaller.STATUS_FAILURE, "No target APK files found for installation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (apkFiles.size > 1) {
|
||||||
|
scope.launch {
|
||||||
|
val success = installApks(lspApp, apkFiles)
|
||||||
|
if (success) {
|
||||||
|
onFinish(
|
||||||
|
PackageInstaller.STATUS_SUCCESS,
|
||||||
|
"Split APKs installed successfully"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onFinish(
|
||||||
|
PackageInstaller.STATUS_FAILURE,
|
||||||
|
"Failed to install split APKs"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installApk(lspApp, apkFiles.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(lifecycleOwner) {
|
||||||
|
val observer = object : DefaultLifecycleObserver {
|
||||||
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED)
|
||||||
|
} else {
|
||||||
|
context.registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
context.unregisterReceiver(splitInstallReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
if (!uninstallFirst) {
|
||||||
|
Log.d(TAG,"Starting installation without uninstalling first")
|
||||||
|
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled")
|
||||||
|
doInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uninstallFirst) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
onFinish(
|
||||||
|
LSPPackageManager.STATUS_USER_CANCELLED,
|
||||||
|
"User cancelled"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "Reset")
|
||||||
|
scope.launch {
|
||||||
|
Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}")
|
||||||
|
uninstallApkByPackageName(lspApp, patchApp.app.packageName)
|
||||||
|
uninstallFirst = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = { Text(stringResource(android.R.string.ok)) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onFinish(
|
||||||
|
LSPPackageManager.STATUS_USER_CANCELLED,
|
||||||
|
"User cancelled"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { Text(stringResource(android.R.string.cancel)) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.uninstall),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(R.string.patch_uninstall_text)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -195,9 +195,6 @@ fun AppManageBody(
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (!ShizukuApi.isPermissionGranted) {
|
|
||||||
snackbarHost.showSnackbar(shizukuUnavailable)
|
|
||||||
} else {
|
|
||||||
viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
|
viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
package org.lsposed.lspatch.ui.util
|
package org.lsposed.lspatch.ui.util
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.lsposed.lspatch.BuildConfig
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
val LazyListState.lastVisibleItemIndex
|
val LazyListState.lastVisibleItemIndex
|
||||||
get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||||
|
|
@ -10,3 +26,134 @@ val LazyListState.lastItemIndex
|
||||||
|
|
||||||
val LazyListState.isScrolledToEnd
|
val LazyListState.isScrolledToEnd
|
||||||
get() = lastVisibleItemIndex == lastItemIndex
|
get() = lastVisibleItemIndex == lastItemIndex
|
||||||
|
|
||||||
|
fun checkIsApkFixedByLSP(context: Context, packageName: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val app =
|
||||||
|
context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
||||||
|
(app.metaData?.containsKey("lspatch") != true)
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
Log.e("LSPatch", "Package not found: $packageName")
|
||||||
|
false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("LSPatch", "Unexpected error in checkIsApkFixedByLSP", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installApk(context: Context, apkFile: File) {
|
||||||
|
try {
|
||||||
|
val apkUri =
|
||||||
|
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile)
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addCategory("android.intent.category.DEFAULT")
|
||||||
|
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallApkByPackageName(context: Context, packageName: String) = try {
|
||||||
|
val intent = Intent(Intent.ACTION_DELETE).apply {
|
||||||
|
data = "package:$packageName".toUri()
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstallResultReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_INSTALL_STATUS = "${BuildConfig.APPLICATION_ID}.INSTALL_STATUS"
|
||||||
|
|
||||||
|
fun createPendingIntent(context: Context, sessionId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, InstallResultReceiver::class.java).apply {
|
||||||
|
action = ACTION_INSTALL_STATUS
|
||||||
|
}
|
||||||
|
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, sessionId, intent, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action != ACTION_INSTALL_STATUS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val status =
|
||||||
|
intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
|
||||||
|
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
val confirmIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||||
|
if (confirmIntent != null) {
|
||||||
|
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageInstaller.STATUS_SUCCESS -> {
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun installApks(context: Context, apkFiles: List<File>): Boolean {
|
||||||
|
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||||
|
data = "package:${context.packageName}".toUri()
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
apkFiles.forEach {
|
||||||
|
if (!it.exists()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
|
var session: PackageInstaller.Session? = null
|
||||||
|
try {
|
||||||
|
val params =
|
||||||
|
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
|
val sessionId = packageInstaller.createSession(params)
|
||||||
|
session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
|
apkFiles.forEach { apkFile ->
|
||||||
|
session.openWrite(apkFile.name, 0, apkFile.length()).use { outputStream ->
|
||||||
|
apkFile.inputStream().use { inputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
session.fsync(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent = InstallResultReceiver.createPendingIntent(context, sessionId)
|
||||||
|
|
||||||
|
session.commit(pendingIntent.intentSender)
|
||||||
|
true
|
||||||
|
} catch (_: IOException) {
|
||||||
|
session?.abandon()
|
||||||
|
false
|
||||||
|
} catch (_: Exception) {
|
||||||
|
session?.abandon()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,10 @@ class NewPatchViewModel : ViewModel() {
|
||||||
INIT, SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
|
INIT, SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class InstallMethod {
|
||||||
|
SYSTEM, SHIZUKU
|
||||||
|
}
|
||||||
|
|
||||||
sealed class ViewAction {
|
sealed class ViewAction {
|
||||||
object DoneInit : ViewAction()
|
object DoneInit : ViewAction()
|
||||||
data class ConfigurePatch(val app: AppInfo) : ViewAction()
|
data class ConfigurePatch(val app: AppInfo) : ViewAction()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import org.lsposed.lspatch.Patcher
|
||||||
import org.lsposed.lspatch.lspApp
|
import org.lsposed.lspatch.lspApp
|
||||||
import org.lsposed.lspatch.share.Constants
|
import org.lsposed.lspatch.share.Constants
|
||||||
import org.lsposed.lspatch.share.PatchConfig
|
import org.lsposed.lspatch.share.PatchConfig
|
||||||
|
import org.lsposed.lspatch.ui.util.installApk
|
||||||
|
import org.lsposed.lspatch.ui.util.installApks
|
||||||
import org.lsposed.lspatch.ui.viewstate.ProcessingState
|
import org.lsposed.lspatch.ui.viewstate.ProcessingState
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager
|
import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
||||||
|
|
@ -89,7 +91,10 @@ class AppManageViewModel : ViewModel() {
|
||||||
updateLoaderState = ProcessingState.Processing
|
updateLoaderState = ProcessingState.Processing
|
||||||
val result = runCatching {
|
val result = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
LSPPackageManager.cleanTmpApkDir()
|
LSPPackageManager.apply {
|
||||||
|
cleanTmpApkDir()
|
||||||
|
cleanExternalTmpApkDir()
|
||||||
|
}
|
||||||
val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray())
|
val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray())
|
||||||
val patchPaths = mutableListOf<String>()
|
val patchPaths = mutableListOf<String>()
|
||||||
val embeddedModulePaths = mutableListOf<String>()
|
val embeddedModulePaths = mutableListOf<String>()
|
||||||
|
|
@ -121,10 +126,23 @@ class AppManageViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths))
|
Patcher.patch(logger, Patcher.Options(false, config, patchPaths, embeddedModulePaths))
|
||||||
|
if (!ShizukuApi.isPermissionGranted) {
|
||||||
|
val apkFiles = lspApp.targetApkFiles
|
||||||
|
if (apkFiles.isNullOrEmpty()){
|
||||||
|
Log.e(TAG, "No patched APK files found")
|
||||||
|
throw RuntimeException("No patched APK files found")
|
||||||
|
}
|
||||||
|
if (apkFiles.size > 1) {
|
||||||
|
val success = installApks(lspApp, apkFiles)
|
||||||
|
} else {
|
||||||
|
installApk(lspApp, apkFiles.first())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val (status, message) = LSPPackageManager.install()
|
val (status, message) = LSPPackageManager.install()
|
||||||
if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)
|
if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
updateLoaderState = ProcessingState.Done(result)
|
updateLoaderState = ProcessingState.Done(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@ object LSPPackageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun cleanExternalTmpApkDir(){
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
lspApp.externalCacheDir?.listFiles()?.forEach(File::delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun install(): Pair<Int, String?> {
|
suspend fun install(): Pair<Int, String?> {
|
||||||
Log.i(TAG, "Perform install patched apks")
|
Log.i(TAG, "Perform install patched apks")
|
||||||
var status = PackageInstaller.STATUS_FAILURE
|
var status = PackageInstaller.STATUS_FAILURE
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<external-path name="external_files" path="." />
|
||||||
|
<files-path name="files" path="." />
|
||||||
|
</paths>
|
||||||
Loading…
Reference in New Issue