Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
19 changes: 19 additions & 0 deletions manager/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<application
android:name=".LSPApplication"
Expand Down Expand Up @@ -33,6 +34,14 @@
</intent-filter>
</activity>

<receiver
android:name=".ui.util.InstallResultReceiver"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.INSTALL_STATUS" />
</intent-filter>
</receiver>

<service
android:name=".manager.ModuleService"
android:exported="true" />
Expand All @@ -44,6 +53,16 @@
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class LSPApplication : Application() {

lateinit var prefs: SharedPreferences
lateinit var tmpApkDir: File
var targetApkFiles: ArrayList<File>? = null

val globalScope = CoroutineScope(Dispatchers.Default)

Expand Down
11 changes: 9 additions & 2 deletions manager/src/main/java/org/lsposed/lspatch/Patcher.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.lsposed.lspatch

import android.util.Log
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
Expand All @@ -10,14 +11,14 @@ import org.lsposed.lspatch.share.Constants
import org.lsposed.lspatch.share.PatchConfig
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
import java.io.File
import java.io.IOException
import java.util.Collections.addAll

object Patcher {

class Options(
private val injectDex: Boolean,
private val config: PatchConfig,
val config: PatchConfig,
private val apkPaths: List<String>,
private val embeddedModules: List<String>?
) {
Expand Down Expand Up @@ -52,19 +53,25 @@ object Patcher {
root.listFiles().forEach {
if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete()
}
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 ->
input.copyTo(output)
}
}
}
lspApp.targetApkFiles = apkFileList
logger.i("Patched files are saved to ${root.uri.lastPathSegment}")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.lsposed.lspatch.ui.activity

import android.annotation.SuppressLint
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
Expand All @@ -8,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
Expand All @@ -21,13 +25,22 @@ import org.lsposed.lspatch.ui.page.appCurrentDestinationAsState
import org.lsposed.lspatch.ui.page.destinations.Destination
import org.lsposed.lspatch.ui.page.startAppDestination
import org.lsposed.lspatch.ui.theme.LSPTheme
import org.lsposed.lspatch.ui.util.InstallResultReceiver
import org.lsposed.lspatch.ui.util.LocalSnackbarHost

class MainActivity : ComponentActivity() {

private val splitInstallReceiver by lazy { InstallResultReceiver() }

@SuppressLint("UnspecifiedRegisterReceiverFlag")
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS), RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(splitInstallReceiver, IntentFilter(InstallResultReceiver.ACTION_INSTALL_STATUS))
}
setContent {
val navController = rememberAnimatedNavController()
LSPTheme {
Expand All @@ -47,6 +60,11 @@ class MainActivity : ComponentActivity() {
}
}
}

override fun onDestroy() {
super.onDestroy()
unregisterReceiver(splitInstallReceiver)
}
}

@Composable
Expand Down
111 changes: 103 additions & 8 deletions manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
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.lastItemIndex
import org.lsposed.lspatch.ui.util.uninstallApkByPackageName
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
Expand Down Expand Up @@ -121,7 +125,10 @@ fun NewPatchScreen(
when (viewModel.patchState) {
PatchState.INIT -> {
LaunchedEffect(Unit) {
LSPPackageManager.cleanTmpApkDir()
LSPPackageManager.apply {
cleanTmpApkDir()
cleanExternalTmpApkDir()
}
when (id) {
ACTION_STORAGE -> {
storageLauncher.launch(arrayOf("application/vnd.android.package-archive"))
Expand Down Expand Up @@ -435,10 +442,10 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
val installSuccessfully = stringResource(R.string.patch_install_successfully)
val installFailed = stringResource(R.string.patch_install_failed)
val copyError = stringResource(R.string.copy_error)
var installing by remember { mutableStateOf(false) }
if (installing) InstallDialog(viewModel.patchApp) { status, message ->
var installing by remember { mutableStateOf(0) }
Copy link
Owner

@JingMatrix JingMatrix Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better, if you use an enum to define installing an rename it to installation.

val onFinish: (Int, String?) -> Unit = { status, message ->
scope.launch {
installing = false
installing = 0
if (status == PackageInstaller.STATUS_SUCCESS) {
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
navigator.navigateUp()
Expand All @@ -451,6 +458,7 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
}
}
}
if (installing == 1) InstallDialog(viewModel.patchApp, onFinish) else if (installing == 2) InstallDialog2(viewModel.patchApp, onFinish)
Row(Modifier.padding(top = 12.dp)) {
Button(
modifier = Modifier.weight(1f),
Expand All @@ -462,11 +470,9 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
modifier = Modifier.weight(1f),
onClick = {
if (!ShizukuApi.isPermissionGranted) {
scope.launch {
snackbarHost.showSnackbar(shizukuUnavailable)
}
installing = 2
} else {
installing = true
installing = 1
}
},
content = { Text(stringResource(R.string.install)) }
Expand Down Expand Up @@ -572,3 +578,92 @@ 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
)
)
}

fun doInstall() {
val apkFiles = lspApp.targetApkFiles
if (apkFiles.isNullOrEmpty()){
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "No target APK files found for installation")
return
}
if (apkFiles.size > 1) {
scope.launch {
val success = installApks(lspApp, apkFiles)
if (success) {
onFinish(
LSPPackageManager.STATUS_USER_CANCELLED,
"Split APKs installed successfully"
)
} else {
onFinish(
LSPPackageManager.STATUS_USER_CANCELLED,
"Failed to install split APKs"
)
}
}
} else {
installApk(lspApp, apkFiles.first())
}
}

LaunchedEffect(Unit) {
if (!uninstallFirst) {
onFinish(LSPPackageManager.STATUS_USER_CANCELLED, "User cancelled")
doInstall()
}
}

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)) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,7 @@ fun AppManageBody(
onClick = {
expanded = false
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))
}
}
)
Expand Down
Loading