From 5d3f0ec9f778d8eec8198e829acf9e35f40e6459 Mon Sep 17 00:00:00 2001 From: Nullptr <52071314+Dr-TSNG@users.noreply.github.com> Date: Thu, 5 May 2022 21:07:45 +0800 Subject: [PATCH] Implement install patched app & Refactor UI --- build.gradle.kts | 4 +- manager/build.gradle.kts | 5 + .../java/org/lsposed/lspatch/Constants.kt | 3 + .../org/lsposed/lspatch/LSPApplication.kt | 38 +- .../main/java/org/lsposed/lspatch/Patcher.kt | 40 ++- .../org/lsposed/lspatch/config/MyKeyStore.kt | 32 +- .../lspatch/ui/activity/MainActivity.kt | 11 +- .../org/lsposed/lspatch/ui/page/HomePage.kt | 34 +- .../org/lsposed/lspatch/ui/page/ManagePage.kt | 41 ++- .../lsposed/lspatch/ui/page/NewPatchPage.kt | 336 ++++++++++-------- .../lsposed/lspatch/ui/page/SettingsPage.kt | 12 +- .../lspatch/ui/util/CompositionProvider.kt | 13 + .../org/lsposed/lspatch/ui/util/Navigation.kt | 6 - .../lspatch/ui/viewmodel/NewPatchViewModel.kt | 42 ++- .../lspatch/util/IntentSenderHelper.kt | 41 +++ .../lspatch/util/LSPPackageInstaller.kt | 97 +++++ .../org/lsposed/lspatch/util/ShizukuApi.kt | 60 ++++ manager/src/main/res/values/strings.xml | 9 +- .../main/java/org/lsposed/patch/LSPatch.java | 3 - settings.gradle.kts | 1 + 20 files changed, 578 insertions(+), 250 deletions(-) create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/util/CompositionProvider.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/util/IntentSenderHelper.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt diff --git a/build.gradle.kts b/build.gradle.kts index f527ca4..93f55fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,8 +46,8 @@ val coreVerName by extra( val androidMinSdkVersion by extra(28) val androidTargetSdkVersion by extra(32) val androidCompileSdkVersion by extra(32) -val androidCompileNdkVersion by extra("23.1.7779620") -val androidBuildToolsVersion by extra("31.0.0") +val androidCompileNdkVersion by extra("24.0.8215888") +val androidBuildToolsVersion by extra("32.0.0") val androidSourceCompatibility by extra(JavaVersion.VERSION_11) val androidTargetCompatibility by extra(JavaVersion.VERSION_11) diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 7550b05..5151bd5 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -5,6 +5,7 @@ val coreVerName: String by rootProject.extra plugins { id("com.android.application") + id("dev.rikka.tools.refine") id("kotlin-parcelize") kotlin("android") } @@ -64,10 +65,13 @@ afterEvaluate { } dependencies { + implementation(projects.hiddenapi.bridge) implementation(projects.patch) implementation(projects.services.daemonService) implementation(projects.share.android) + compileOnly("dev.rikka.hidden:stub:2.3.1") + implementation("dev.rikka.hidden:compat:2.3.1") implementation("androidx.core:core-ktx:1.7.0") implementation("androidx.activity:activity-compose:1.6.0-alpha01") implementation("androidx.compose.material:material-icons-extended:1.1.1") @@ -84,4 +88,5 @@ dependencies { implementation("com.google.android.material:material:1.5.0") implementation("dev.rikka.shizuku:api:12.1.0") implementation("dev.rikka.shizuku:provider:12.1.0") + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") } diff --git a/manager/src/main/java/org/lsposed/lspatch/Constants.kt b/manager/src/main/java/org/lsposed/lspatch/Constants.kt index 27dae05..977822d 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Constants.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Constants.kt @@ -2,5 +2,8 @@ package org.lsposed.lspatch object Constants { + const val PREFS_KEYSTORE_PASSWORD = "keystore_password" + const val PREFS_KEYSTORE_ALIAS = "keystore_alias" + const val PREFS_KEYSTORE_ALIAS_PASSWORD = "keystore_alias_password" const val PREFS_STORAGE_DIRECTORY = "storage_directory" } diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt index 7103c22..4151af2 100644 --- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt @@ -3,39 +3,27 @@ package org.lsposed.lspatch import android.app.Application import android.content.Context import android.content.SharedPreferences -import android.content.pm.PackageManager -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import rikka.shizuku.Shizuku +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.lsposed.hiddenapibypass.HiddenApiBypass +import org.lsposed.lspatch.util.ShizukuApi const val TAG = "LSPatch Manager" +lateinit var lspApp: LSPApplication + class LSPApplication : Application() { - companion object { - var shizukuBinderAvalable = false - var shizukuGranted by mutableStateOf(false) + lateinit var prefs: SharedPreferences - lateinit var appContext: Context - lateinit var prefs: SharedPreferences - - init { - Shizuku.addBinderReceivedListenerSticky { - shizukuBinderAvalable = true - shizukuGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED - } - Shizuku.addBinderDeadListener { - shizukuBinderAvalable = false - shizukuGranted = false - } - } - } + val globalScope = CoroutineScope(Dispatchers.Default) override fun onCreate() { super.onCreate() - appContext = applicationContext - appContext.filesDir.mkdir() - prefs = appContext.getSharedPreferences("settings", Context.MODE_PRIVATE) + HiddenApiBypass.addHiddenApiExemptions(""); + lspApp = this + lspApp.filesDir.mkdir() + prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE) + ShizukuApi.init() } } diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 50653c5..7b814af 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -5,38 +5,37 @@ import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.patch.LSPatch import org.lsposed.patch.util.Logger import java.io.File +import java.io.IOException import java.nio.file.Files -import kotlin.io.path.absolutePathString object Patcher { + class Options( private val apkPaths: List, private val debuggable: Boolean, private val sigbypassLevel: Int, private val v1: Boolean, private val v2: Boolean, - private val v3: Boolean, private val useManager: Boolean, private val overrideVersionCode: Boolean, private val verbose: Boolean, private val embeddedModules: List? ) { - lateinit var outputPath: String + lateinit var outputDir: File fun toStringArray(): Array { return buildList { - add("-f") addAll(apkPaths) - add("-o"); add(outputPath) + add("-o"); add(outputDir.absolutePath) if (debuggable) add("-d") add("-l"); add(sigbypassLevel.toString()) add("--v1"); add(v1.toString()) add("--v2"); add(v2.toString()) - add("--v3"); add(v3.toString()) if (useManager) add("--manager") if (overrideVersionCode) add("-r") if (verbose) add("-v") @@ -52,24 +51,27 @@ object Patcher { suspend fun patch(context: Context, logger: Logger, options: Options) { withContext(Dispatchers.IO) { - options.outputPath = Files.createTempDirectory("patch").absolutePathString() + options.outputDir = Files.createTempDirectory("patch").toFile() + options.outputDir.listFiles()?.forEach(File::delete) LSPatch(logger, *options.toStringArray()).doCommandLine() - val uri = LSPApplication.prefs.getString(Constants.PREFS_STORAGE_DIRECTORY, null)?.toUri() - ?: throw IllegalStateException("Uri is null") + val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri() + ?: throw IOException("Uri is null") val root = DocumentFile.fromTreeUri(context, uri) - ?: throw IllegalStateException("DocumentFile is null") - root.listFiles().forEach { it.delete() } - File(options.outputPath) + ?: throw IOException("DocumentFile is null") + root.listFiles().forEach { + if (it.name?.endsWith("-lspatched.apk") == true) it.delete() + } + options.outputDir .walk() .filter { it.isFile } - .forEach { - val file = root.createFile("application/vnd.android.package-archive", it.name) - ?: throw IllegalStateException("Failed to create output file") - val os = context.contentResolver.openOutputStream(file.uri) - ?: throw IllegalStateException("Failed to open output stream") - os.use { output -> - it.inputStream().use { input -> + .forEach { apk -> + val file = root.createFile("application/vnd.android.package-archive", apk.name) + ?: throw IOException("Failed to create output file") + val output = context.contentResolver.openOutputStream(file.uri) + ?: throw IOException("Failed to open output stream") + output.use { + apk.inputStream().use { input -> input.copyTo(output) } } diff --git a/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt b/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt index 7a36943..3e7b1c3 100644 --- a/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt +++ b/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt @@ -6,24 +6,26 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.lsposed.lspatch.LSPApplication.Companion.appContext -import org.lsposed.lspatch.LSPApplication.Companion.prefs +import org.lsposed.lspatch.Constants.PREFS_KEYSTORE_ALIAS +import org.lsposed.lspatch.Constants.PREFS_KEYSTORE_ALIAS_PASSWORD +import org.lsposed.lspatch.Constants.PREFS_KEYSTORE_PASSWORD +import org.lsposed.lspatch.lspApp import java.io.File object MyKeyStore { - val file = File("${appContext.filesDir}/keystore.bks") + val file = File("${lspApp.filesDir}/keystore.bks") - val tmpFile = File("${appContext.filesDir}/keystore.bks.tmp") + val tmpFile = File("${lspApp.filesDir}/keystore.bks.tmp") val password: String - get() = prefs.getString("keystore_password", "123456")!! + get() = lspApp.prefs.getString("keystore_password", "123456")!! val alias: String - get() = prefs.getString("keystore_alias", "key0")!! + get() = lspApp.prefs.getString("keystore_alias", "key0")!! val aliasPassword: String - get() = prefs.getString("keystore_alias_password", "123456")!! + get() = lspApp.prefs.getString("keystore_alias_password", "123456")!! private var mUseDefault by mutableStateOf(!file.exists()) val useDefault by derivedStateOf { mUseDefault } @@ -31,10 +33,10 @@ object MyKeyStore { suspend fun reset() { withContext(Dispatchers.IO) { file.delete() - prefs.edit() - .putString("keystore_password", "123456") - .putString("keystore_alias", "key0") - .putString("keystore_alias_password", "123456") + lspApp.prefs.edit() + .putString(PREFS_KEYSTORE_PASSWORD, "123456") + .putString(PREFS_KEYSTORE_ALIAS, "key0") + .putString(PREFS_KEYSTORE_ALIAS_PASSWORD, "123456") .apply() mUseDefault = true } @@ -43,10 +45,10 @@ object MyKeyStore { suspend fun setCustom(password: String, alias: String, aliasPassword: String) { withContext(Dispatchers.IO) { tmpFile.renameTo(file) - prefs.edit() - .putString("keystore_password", password) - .putString("keystore_alias", alias) - .putString("keystore_alias_password", aliasPassword) + lspApp.prefs.edit() + .putString(PREFS_KEYSTORE_PASSWORD, password) + .putString(PREFS_KEYSTORE_ALIAS, alias) + .putString(PREFS_KEYSTORE_ALIAS_PASSWORD, aliasPassword) .apply() mUseDefault = false } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt index 04ca23d..b7acf17 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt @@ -17,6 +17,7 @@ import com.google.accompanist.navigation.animation.rememberAnimatedNavController import org.lsposed.lspatch.ui.page.PageList import org.lsposed.lspatch.ui.theme.LSPTheme import org.lsposed.lspatch.ui.util.LocalNavController +import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.currentRoute class MainActivity : ComponentActivity() { @@ -27,11 +28,14 @@ class MainActivity : ComponentActivity() { setContent { val navController = rememberAnimatedNavController() val currentRoute = navController.currentRoute - val currentPage = if (currentRoute == null) null else PageList.valueOf(currentRoute.substringBefore('/')) var mainPage by rememberSaveable { mutableStateOf(PageList.Home) } LSPTheme { - CompositionLocalProvider(LocalNavController provides navController) { + val snackbarHostState = remember { SnackbarHostState() } + CompositionLocalProvider( + LocalNavController provides navController, + LocalSnackbarHost provides snackbarHostState + ) { Scaffold( bottomBar = { MainNavigationBar(mainPage) { @@ -42,7 +46,8 @@ class MainActivity : ComponentActivity() { } } } - } + }, + snackbarHost = { SnackbarHost(snackbarHostState) } ) { innerPadding -> MainNavHost(navController, Modifier.padding(innerPadding)) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt index 2897ac4..bb9d469 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt @@ -24,19 +24,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.lsposed.lspatch.BuildConfig -import org.lsposed.lspatch.LSPApplication import org.lsposed.lspatch.R import org.lsposed.lspatch.ui.util.HtmlText +import org.lsposed.lspatch.ui.util.LocalSnackbarHost +import org.lsposed.lspatch.util.ShizukuApi import rikka.shizuku.Shizuku @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomePage() { - val snackbarHostState = remember { SnackbarHostState() } - Scaffold( - topBar = { TopBar() }, - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { innerPadding -> + Scaffold(topBar = { TopBar() }) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) @@ -45,7 +42,7 @@ fun HomePage() { ) { ShizukuCard() Spacer(Modifier.height(16.dp)) - InfoCard(snackbarHostState) + InfoCard() Spacer(Modifier.height(16.dp)) SupportCard() } @@ -64,12 +61,12 @@ private fun TopBar() { fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.titleMedium ) - }, + } ) } private val listener: (Int, Int) -> Unit = { _, grantResult -> - LSPApplication.shizukuGranted = grantResult == PackageManager.PERMISSION_GRANTED + ShizukuApi.isPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED } @OptIn(ExperimentalMaterial3Api::class) @@ -87,12 +84,12 @@ private fun ShizukuCard() { ElevatedCard( modifier = Modifier.clickable { - if (LSPApplication.shizukuBinderAvalable && !LSPApplication.shizukuGranted) { + if (ShizukuApi.isBinderAvalable && !ShizukuApi.isPermissionGranted) { Shizuku.requestPermission(114514) } }, containerColor = run { - if (LSPApplication.shizukuGranted) MaterialTheme.colorScheme.secondaryContainer + if (ShizukuApi.isPermissionGranted) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer } ) { @@ -102,11 +99,11 @@ private fun ShizukuCard() { .padding(24.dp), verticalAlignment = Alignment.CenterVertically ) { - if (LSPApplication.shizukuGranted) { - Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_shizuku_available)) + if (ShizukuApi.isPermissionGranted) { + Icon(Icons.Outlined.CheckCircle, stringResource(R.string.shizuku_available)) Column(Modifier.padding(start = 20.dp)) { Text( - text = stringResource(R.string.home_shizuku_available), + text = stringResource(R.string.shizuku_available), fontFamily = FontFamily.Serif, style = MaterialTheme.typography.titleMedium ) @@ -117,10 +114,10 @@ private fun ShizukuCard() { ) } } else { - Icon(Icons.Outlined.Warning, stringResource(R.string.home_shizuku_unavailable)) + Icon(Icons.Outlined.Warning, stringResource(R.string.shizuku_unavailable)) Column(Modifier.padding(start = 20.dp)) { Text( - text = stringResource(R.string.home_shizuku_unavailable), + text = stringResource(R.string.shizuku_unavailable), fontFamily = FontFamily.Serif, style = MaterialTheme.typography.titleMedium ) @@ -151,8 +148,9 @@ private val device = buildString { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun InfoCard(snackbarHostState: SnackbarHostState) { +private fun InfoCard() { val context = LocalContext.current + val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() ElevatedCard { Column( @@ -190,7 +188,7 @@ private fun InfoCard(snackbarHostState: SnackbarHostState) { onClick = { val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("LSPatch", contents.toString())) - scope.launch { snackbarHostState.showSnackbar(copiedMessage) } + scope.launch { snackbarHost.showSnackbar(copiedMessage) } }, content = { Text(stringResource(android.R.string.copy)) } ) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt index f045b9a..7d44141 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/ManagePage.kt @@ -5,29 +5,31 @@ import android.content.Intent import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.launch -import org.lsposed.lspatch.Constants -import org.lsposed.lspatch.LSPApplication +import org.lsposed.lspatch.* +import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.R -import org.lsposed.lspatch.TAG import org.lsposed.lspatch.ui.util.LocalNavController +import org.lsposed.lspatch.ui.util.LocalSnackbarHost import java.io.IOException @OptIn(ExperimentalMaterial3Api::class) @Composable fun ManagePage() { - val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { TopBar() }, - floatingActionButton = { Fab(snackbarHostState) }, - snackbarHost = { SnackbarHost(snackbarHostState) } + floatingActionButton = { Fab() } ) { innerPadding -> } @@ -41,8 +43,9 @@ private fun TopBar() { } @Composable -private fun Fab(snackbarHostState: SnackbarHostState) { +private fun Fab() { val context = LocalContext.current + val snackbarHost = LocalSnackbarHost.current val navController = LocalNavController.current val scope = rememberCoroutineScope() var shouldSelectDirectory by remember { mutableStateOf(false) } @@ -54,12 +57,12 @@ private fun Fab(snackbarHostState: SnackbarHostState) { val uri = it.data?.data ?: throw IOException("No data") val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.takePersistableUriPermission(uri, takeFlags) - LSPApplication.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, uri.toString()).apply() + lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, uri.toString()).apply() Log.i(TAG, "Storage directory: ${uri.path}") navController.navigate(PageList.NewPatch.name) } catch (e: Exception) { Log.e(TAG, "Error when requesting saving directory", e) - scope.launch { snackbarHostState.showSnackbar(errorText) } + scope.launch { snackbarHost.showSnackbar(errorText) } } } @@ -81,7 +84,13 @@ private fun Fab(snackbarHostState: SnackbarHostState) { onClick = { shouldSelectDirectory = false } ) }, - title = { Text(stringResource(R.string.patch_select_dir_title)) }, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.patch_select_dir_title), + textAlign = TextAlign.Center + ) + }, text = { Text(stringResource(R.string.patch_select_dir_text)) } ) } @@ -89,17 +98,19 @@ private fun Fab(snackbarHostState: SnackbarHostState) { FloatingActionButton( content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) }, onClick = { - val uri = LSPApplication.prefs.getString(Constants.PREFS_STORAGE_DIRECTORY, null)?.toUri() + val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri() if (uri == null) { shouldSelectDirectory = true } else { - try { + runCatching { val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.takePersistableUriPermission(uri, takeFlags) + if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted") + }.onSuccess { navController.navigate(PageList.NewPatch.name) - } catch (e: SecurityException) { - Log.e(TAG, "Failed to take persistable permission for saved uri", e) - LSPApplication.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, null).apply() + }.onFailure { + Log.w(TAG, "Failed to take persistable permission for saved uri", it) + lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, null).apply() shouldSelectDirectory = true } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt index f462aa1..3593bd5 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt @@ -3,6 +3,7 @@ package org.lsposed.lspatch.ui.page import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.pm.PackageInstaller import android.util.Log import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring @@ -25,81 +26,71 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry +import kotlinx.coroutines.launch import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.R import org.lsposed.lspatch.TAG +import org.lsposed.lspatch.lspApp 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.SettingsItem -import org.lsposed.lspatch.ui.util.LocalNavController -import org.lsposed.lspatch.ui.util.isScrolledToEnd -import org.lsposed.lspatch.ui.util.lastItemIndex -import org.lsposed.lspatch.ui.util.observeState +import org.lsposed.lspatch.ui.util.* import org.lsposed.lspatch.ui.viewmodel.AppInfo import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel +import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState +import org.lsposed.lspatch.util.LSPPackageInstaller +import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.patch.util.Logger -import java.io.File - -private enum class PatchState { - SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED, ERROR -} @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewPatchPage(entry: NavBackStackEntry) { + val viewModel = viewModel() val navController = LocalNavController.current - val patchApp by entry.observeState("appInfo") + val lifecycleOwner = LocalLifecycleOwner.current val isCancelled by entry.observeState("isCancelled") - var patchState by rememberSaveable { mutableStateOf(PatchState.SELECTING) } - var patchOptions by rememberSaveable { mutableStateOf(null) } - if (patchState == PatchState.SELECTING) { - when { - isCancelled == true -> { - LaunchedEffect(entry) { navController.popBackStack() } - return - } - patchApp != null -> patchState = PatchState.CONFIGURING - } + entry.savedStateHandle.getLiveData("appInfo").observe(lifecycleOwner) { + viewModel.patchApp = it } - Log.d(TAG, "NewPatchPage: $patchState") - if (patchState == PatchState.SELECTING) { - LaunchedEffect(entry) { - navController.navigate(PageList.SelectApps.name + "/false") + Log.d(TAG, "NewPatchPage: ${viewModel.patchState}") + if (viewModel.patchState == PatchState.SELECTING) { + when { + isCancelled == true -> { + LaunchedEffect(viewModel) { navController.popBackStack() } + return + } + viewModel.patchApp != null -> { + LaunchedEffect(viewModel) { viewModel.configurePatch() } + } + else -> { + LaunchedEffect(viewModel) { navController.navigate(PageList.SelectApps.name + "/false") } + } } } else { Scaffold( - topBar = { TopBar(patchApp!!) }, + topBar = { TopBar(viewModel.patchApp!!) }, floatingActionButton = { - if (patchState == PatchState.CONFIGURING) { - ConfiguringFab { patchState = PatchState.SUBMITTING } + if (viewModel.patchState == PatchState.CONFIGURING) { + ConfiguringFab() } } ) { innerPadding -> - if (patchState == PatchState.CONFIGURING || patchState == PatchState.SUBMITTING) { - PatchOptionsBody( - modifier = Modifier.padding(innerPadding), - patchState = patchState, - patchApp = patchApp!!, - onSubmit = { - patchOptions = it - patchState = PatchState.PATCHING - } - ) + if (viewModel.patchState == PatchState.CONFIGURING) { + entry.savedStateHandle.getLiveData>("selected", SnapshotStateList()).observe(lifecycleOwner) { + viewModel.embeddedModules = it + } + PatchOptionsBody(Modifier.padding(innerPadding)) } else { - DoPatchBody( - modifier = Modifier.padding(innerPadding), - patchState = patchState, - patchOptions = patchOptions!!, - onFinish = { patchState = PatchState.FINISHED }, - onFail = { patchState = PatchState.ERROR } - ) + DoPatchBody(Modifier.padding(innerPadding)) } } } @@ -126,11 +117,12 @@ private fun TopBar(patchApp: AppInfo) { } @Composable -private fun ConfiguringFab(onClick: () -> Unit) { +private fun ConfiguringFab() { + val viewModel = viewModel() ExtendedFloatingActionButton( text = { Text(stringResource(R.string.patch_start)) }, icon = { Icon(Icons.Outlined.AutoFixHigh, null) }, - onClick = onClick + onClick = { viewModel.submitPatch() } ) } @@ -144,31 +136,9 @@ private fun sigBypassLvStr(level: Int) = when (level) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun PatchOptionsBody( - modifier: Modifier, - patchState: PatchState, - patchApp: AppInfo, - onSubmit: (Patcher.Options) -> Unit -) { - val navController = LocalNavController.current +private fun PatchOptionsBody(modifier: Modifier) { val viewModel = viewModel() - val embeddedModules = navController.currentBackStackEntry!! - .savedStateHandle.getLiveData>("selected", SnapshotStateList()) - - if (patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) { - if (viewModel.useManager) embeddedModules.value?.clear() - val options = Patcher.Options( - apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()), - debuggable = viewModel.debuggable, - sigbypassLevel = viewModel.sigBypassLevel, - v1 = viewModel.sign[0], v2 = viewModel.sign[1], v3 = viewModel.sign[2], - useManager = viewModel.useManager, - overrideVersionCode = viewModel.overrideVersionCode, - verbose = true, - embeddedModules = embeddedModules.value?.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } - ) - onSubmit(options) - } + val navController = LocalNavController.current Column(modifier.verticalScroll(rememberScrollState())) { Text( @@ -194,10 +164,9 @@ private fun PatchOptionsBody( desc = stringResource(R.string.patch_portable_desc), extraContent = { TextButton( - onClick = { navController.navigate(PageList.SelectApps.name + "/true") } - ) { - Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) - } + onClick = { navController.navigate(PageList.SelectApps.name + "/true") }, + content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) } + ) } ) } @@ -265,54 +234,51 @@ private fun PatchOptionsBody( } } -@Composable -private fun DoPatchBody( - modifier: Modifier, - patchState: PatchState, - patchOptions: Patcher.Options, - onFinish: () -> Unit, - onFail: () -> Unit -) { - val context = LocalContext.current - val navController = LocalNavController.current - val logs = remember { mutableStateListOf>() } - val logger = remember { - object : Logger() { - override fun d(msg: String) { - if (verbose) { - Log.d(TAG, msg) - logs += Log.DEBUG to msg - } - } - - override fun i(msg: String) { - Log.i(TAG, msg) - logs += Log.INFO to msg - } - - override fun e(msg: String) { - Log.e(TAG, msg) - logs += Log.ERROR to msg - } +private class PatchLogger(private val logs: MutableList>) : Logger() { + override fun d(msg: String) { + if (verbose) { + Log.d(TAG, msg) + logs += Log.DEBUG to msg } } - LaunchedEffect(patchOptions) { + override fun i(msg: String) { + Log.i(TAG, msg) + logs += Log.INFO to msg + } + + override fun e(msg: String) { + Log.e(TAG, msg) + logs += Log.ERROR to msg + } +} + +@Composable +private fun DoPatchBody(modifier: Modifier) { + val viewModel = viewModel() + val context = LocalContext.current + val snackbarHost = LocalSnackbarHost.current + val navController = LocalNavController.current + val scope = rememberCoroutineScope() + val logs = remember { mutableStateListOf>() } + val logger = remember { PatchLogger(logs) } + + LaunchedEffect(viewModel) { try { - Patcher.patch(context, logger, patchOptions) - onFinish() + Patcher.patch(context, logger, viewModel.patchOptions) + viewModel.finishPatch() } catch (t: Throwable) { logger.e(t.message.orEmpty()) logger.e(t.stackTraceToString()) - onFail() + viewModel.failPatch() } finally { - File(patchOptions.outputPath).deleteRecursively() + viewModel.patchOptions.outputDir.deleteRecursively() } } BoxWithConstraints(modifier.padding(24.dp)) { val shellBoxMaxHeight = - if (patchState == PatchState.PATCHING) maxHeight + if (viewModel.patchState == PatchState.PATCHING) maxHeight else maxHeight - ButtonDefaults.MinHeight - 12.dp Column( Modifier @@ -320,7 +286,7 @@ private fun DoPatchBody( .wrapContentHeight() .animateContentSize(spring(stiffness = Spring.StiffnessLow)) ) { - ShimmerAnimation(enabled = patchState == PatchState.PATCHING) { + ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) { CompositionLocalProvider( LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace) ) { @@ -351,38 +317,130 @@ private fun DoPatchBody( } } - if (patchState == PatchState.FINISHED) { - Row(Modifier.padding(top = 12.dp)) { - Button( - onClick = { navController.popBackStack() }, - modifier = Modifier.weight(1f), - content = { Text(stringResource(R.string.patch_return)) } - ) - Spacer(Modifier.weight(0.2f)) - Button( - onClick = { /* TODO: Install */ }, - modifier = Modifier.weight(1f), - content = { Text(stringResource(R.string.patch_install)) } - ) + when (viewModel.patchState) { + 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) + var installing by rememberSaveable { mutableStateOf(false) } + if (installing) InstallDialog(viewModel.patchApp!!) { status, message -> + installing = false + scope.launch { + if (status == PackageInstaller.STATUS_SUCCESS) { + lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) } + navController.popBackStack() + } else { + snackbarHost.showSnackbar(installFailed) + } + } + } + Row(Modifier.padding(top = 12.dp)) { + Button( + modifier = Modifier.weight(1f), + onClick = { navController.popBackStack() }, + content = { Text(stringResource(R.string.patch_return)) } + ) + Spacer(Modifier.weight(0.2f)) + Button( + modifier = Modifier.weight(1f), + onClick = { + if (!ShizukuApi.isPermissionGranted) { + scope.launch { + snackbarHost.showSnackbar(shizukuUnavailable) + } + } + installing = true + }, + content = { Text(stringResource(R.string.patch_install)) } + ) + } } - } else if (patchState == PatchState.ERROR) { - Row(Modifier.padding(top = 12.dp)) { - Button( - onClick = { navController.popBackStack() }, - modifier = Modifier.weight(1f), - content = { Text(stringResource(R.string.patch_return)) } - ) - Spacer(Modifier.weight(0.2f)) - Button( - onClick = { - val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip(ClipData.newPlainText("LSPatch", logs.joinToString { it.second + "\n" })) - }, - modifier = Modifier.weight(1f), - content = { Text(stringResource(R.string.patch_copy_error)) } - ) + PatchState.ERROR -> { + Row(Modifier.padding(top = 12.dp)) { + Button( + modifier = Modifier.weight(1f), + onClick = { navController.popBackStack() }, + content = { Text(stringResource(R.string.patch_return)) } + ) + Spacer(Modifier.weight(0.2f)) + Button( + modifier = Modifier.weight(1f), + onClick = { + val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("LSPatch", logs.joinToString { it.second + "\n" })) + }, + content = { Text(stringResource(R.string.patch_copy_error)) } + ) + } } + else -> Unit } } } } + +@Composable +private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { + val scope = rememberCoroutineScope() + + var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalled(patchApp.app.packageName)) } + var installing by remember { mutableStateOf(false) } + val doInstall = suspend { + Log.i(TAG, "Installing app ${patchApp.app.packageName}") + installing = true + val (status, message) = LSPPackageInstaller.install() + installing = false + Log.i(TAG, "Installation end: $status, $message") + onFinish(status, message) + } + + if (uninstallFirst) { + AlertDialog( + onDismissRequest = { onFinish(-2, "User cancelled") }, + confirmButton = { + TextButton( + onClick = { + scope.launch { + Log.i(TAG, "Uninstalling app ${patchApp.app.packageName}") + val (status, message) = LSPPackageInstaller.uninstall(patchApp.app.packageName) + Log.i(TAG, "Uninstallation end: $status, $message") + if (status != PackageInstaller.STATUS_SUCCESS) onFinish(status, message) + uninstallFirst = false + doInstall() + } + }, + content = { Text(stringResource(android.R.string.ok)) } + ) + }, + dismissButton = { + TextButton( + onClick = { onFinish(-2, "User cancelled") }, + content = { Text(stringResource(android.R.string.cancel)) } + ) + }, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.patch_uninstall), + textAlign = TextAlign.Center + ) + }, + text = { Text(stringResource(R.string.patch_uninstall_text)) } + ) + } + + if (installing) { + AlertDialog( + onDismissRequest = {}, + confirmButton = {}, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.patch_installing), + fontFamily = FontFamily.Serif, + textAlign = TextAlign.Center + ) + } + ) + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt index 8297506..699dda0 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -19,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.lsposed.lspatch.R @@ -150,7 +152,13 @@ private fun KeyStore() { onClick = { dropDownExpanded = false; showDialog = false } ) }, - title = { Text(stringResource(R.string.settings_keystore_dialog_title)) }, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.settings_keystore_dialog_title), + textAlign = TextAlign.Center + ) + }, text = { Column { val interactionSource = remember { MutableInteractionSource() } @@ -163,7 +171,7 @@ private fun KeyStore() { } val wrongText = when { - wrongAliasPassword -> stringResource(R.string.settings_keystore_wrong_keystore) + wrongAliasPassword -> stringResource(R.string.settings_keystore_wrong_alias_password) wrongAliasName -> stringResource(R.string.settings_keystore_wrong_alias) wrongPassword -> stringResource(R.string.settings_keystore_wrong_password) wrongKeystore -> stringResource(R.string.settings_keystore_wrong_keystore) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/CompositionProvider.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/CompositionProvider.kt new file mode 100644 index 0000000..9502419 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/CompositionProvider.kt @@ -0,0 +1,13 @@ +package org.lsposed.lspatch.ui.util + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.compositionLocalOf +import androidx.navigation.NavHostController + +val LocalNavController = compositionLocalOf { + error("CompositionLocal LocalNavController not present") +} + +val LocalSnackbarHost = compositionLocalOf { + error("CompositionLocal LocalSnackbarController not present") +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt index c301a8d..8be2ddb 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt @@ -1,18 +1,12 @@ package org.lsposed.lspatch.ui.util import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.livedata.observeAsState import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState -val LocalNavController = compositionLocalOf { - error("CompositionLocal LocalNavController not present") -} - val NavController.currentRoute: String? @Composable get() = currentBackStackEntryAsState().value?.destination?.route diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt index 178d5ad..e2bb48b 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt @@ -4,13 +4,53 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.ViewModel +import org.lsposed.lspatch.Patcher class NewPatchViewModel : ViewModel() { + enum class PatchState { + SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR + } + + var patchState by mutableStateOf(PatchState.SELECTING) + private set + var patchApp by mutableStateOf(null) + var useManager by mutableStateOf(true) var debuggable by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false) - var sign = mutableStateListOf(false, true, true) + var sign = mutableStateListOf(false, true) var sigBypassLevel by mutableStateOf(2) + + lateinit var embeddedModules: SnapshotStateList + lateinit var patchOptions: Patcher.Options + + fun configurePatch() { + patchState = PatchState.CONFIGURING + } + + fun submitPatch() { + if (useManager) embeddedModules.clear() + patchOptions = Patcher.Options( + apkPaths = listOf(patchApp!!.app.sourceDir) + (patchApp!!.app.splitSourceDirs ?: emptyArray()), + debuggable = debuggable, + sigbypassLevel = sigBypassLevel, + v1 = sign[0], v2 = sign[1], + useManager = useManager, + overrideVersionCode = overrideVersionCode, + verbose = true, + embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } + ) + patchState = PatchState.PATCHING + } + + fun finishPatch() { + patchState = PatchState.FINISHED + } + + fun failPatch() { + patchState = PatchState.ERROR + } } diff --git a/manager/src/main/java/org/lsposed/lspatch/util/IntentSenderHelper.kt b/manager/src/main/java/org/lsposed/lspatch/util/IntentSenderHelper.kt new file mode 100644 index 0000000..515ab75 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/util/IntentSenderHelper.kt @@ -0,0 +1,41 @@ +package org.lsposed.lspatch.util + +import android.content.IIntentReceiver +import android.content.IIntentSender +import android.content.Intent +import android.content.IntentSender +import android.os.Bundle +import android.os.IBinder + +object IntentSenderHelper { + + fun newIntentSender(binder: IIntentSender): IntentSender { + return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(binder) + } + + class IIntentSenderAdaptor(private val listener: (Intent) -> Unit) : IIntentSender.Stub() { + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ): Int { + listener(intent) + return 0 + } + + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + listener(intent) + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt new file mode 100644 index 0000000..f7a7cca --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageInstaller.kt @@ -0,0 +1,97 @@ +package org.lsposed.lspatch.util + +import android.content.Intent +import android.content.pm.PackageInstaller +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import hidden.HiddenApiBridge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY +import org.lsposed.lspatch.lspApp +import java.io.IOException +import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +object LSPPackageInstaller { + + suspend fun install(): Pair { + var status = PackageInstaller.STATUS_FAILURE + var message: String? = null + withContext(Dispatchers.IO) { + runCatching { + val params = PackageInstaller.SessionParams::class.java.getConstructor(Int::class.javaPrimitiveType) + .newInstance(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + var flags = HiddenApiBridge.PackageInstaller_SessionParams_installFlags(params) + flags = flags or 0x00000004 /* PackageManager.INSTALL_ALLOW_TEST */ or 0x00000002 /* PackageManager.INSTALL_REPLACE_EXISTING */ + HiddenApiBridge.PackageInstaller_SessionParams_installFlags(params, flags) + ShizukuApi.createPackageInstallerSession(params).use { session -> + val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri() + ?: throw IOException("Uri is null") + val root = DocumentFile.fromTreeUri(lspApp, uri) + ?: throw IOException("DocumentFile is null") + root.listFiles().forEach { apk -> + val input = lspApp.contentResolver.openInputStream(apk.uri) + ?: throw IOException("Cannot open input stream") + input.use { + session.openWrite(apk.name!!, 0, input.available().toLong()).use { output -> + input.copyTo(output) + session.fsync(output) + } + } + } + var result: Intent? = null + suspendCoroutine { cont -> + val countDownLatch = CountDownLatch(1) + val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> + result = intent + countDownLatch.countDown() + } + val intentSender = IntentSenderHelper.newIntentSender(adapter) + session.commit(intentSender) + countDownLatch.await() + cont.resume(Unit) + } + result?.let { + status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + } ?: throw IOException("Intent is null") + } + }.onFailure { + status = PackageInstaller.STATUS_FAILURE + message = it.message + "\n" + it.stackTraceToString() + } + } + return Pair(status, message) + } + + suspend fun uninstall(packageName: String): Pair { + var status = PackageInstaller.STATUS_FAILURE + var message: String? = null + withContext(Dispatchers.IO) { + runCatching { + var result: Intent? = null + suspendCoroutine { cont -> + val countDownLatch = CountDownLatch(1) + val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> + result = intent + countDownLatch.countDown() + } + val intentSender = IntentSenderHelper.newIntentSender(adapter) + ShizukuApi.uninstallPackage(packageName, intentSender) + countDownLatch.await() + cont.resume(Unit) + } + result?.let { + status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + } ?: throw IOException("Intent is null") + }.onFailure { + status = PackageInstaller.STATUS_FAILURE + message = it.message + "\n" + it.stackTraceToString() + } + } + return Pair(status, message) + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt b/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt new file mode 100644 index 0000000..2ce4ad6 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt @@ -0,0 +1,60 @@ +package org.lsposed.lspatch.util + +import android.content.IntentSender +import android.content.pm.* +import android.os.IBinder +import android.os.IInterface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuBinderWrapper +import rikka.shizuku.SystemServiceHelper + +object ShizukuApi { + + private fun IBinder.wrap() = ShizukuBinderWrapper(this) + private fun IInterface.asShizukuBinder() = this.asBinder().wrap() + + private val iPackageManager: IPackageManager by lazy { + IPackageManager.Stub.asInterface(SystemServiceHelper.getSystemService("package").wrap()) + } + + private val iPackageInstaller: IPackageInstaller by lazy { + IPackageInstaller.Stub.asInterface(iPackageManager.packageInstaller.asShizukuBinder()) + } + + private val packageInstaller: PackageInstaller by lazy { + PackageInstaller::class.java.getConstructor(IPackageInstaller::class.java, String::class.java, Int::class.javaPrimitiveType) + .newInstance(iPackageInstaller, "com.android.shell", 0) + } + + var isBinderAvalable = false + var isPermissionGranted by mutableStateOf(false) + + fun init() { + Shizuku.addBinderReceivedListenerSticky { + isBinderAvalable = true + isPermissionGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + } + Shizuku.addBinderDeadListener { + isBinderAvalable = false + isPermissionGranted = false + } + } + + fun createPackageInstallerSession(params: PackageInstaller.SessionParams): PackageInstaller.Session { + val sessionId = packageInstaller.createSession(params) + val iSession = IPackageInstallerSession.Stub.asInterface(iPackageInstaller.openSession(sessionId).asShizukuBinder()) + val constructor by lazy { PackageInstaller.Session::class.java.getConstructor(IPackageInstallerSession::class.java) } + return constructor.newInstance(iSession) + } + + fun isPackageInstalled(packageName: String): Boolean { + return iPackageManager.getPackageInfo(packageName, 0, 0 /* TODO: userId */) != null + } + + fun uninstallPackage(packageName: String, intentSender: IntentSender) { + packageInstaller.uninstall(packageName, intentSender) + } +} diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 556828b..62fa942 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -1,12 +1,12 @@ Add + Shizuku service available + Shizuku service not connected Repo Logs - Shizuku service available - Shizuku service not connected Some functions unavailable API Version LSPatch Version @@ -44,6 +44,11 @@ Start Patch Return Install + Installing + Uninstall + Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data. + Install successfully + Install failed Copy error diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index 5728f0a..e79de8e 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -82,9 +82,6 @@ public class LSPatch { @Parameter(names = {"--v2"}, arity = 1, description = "Sign with v2 signature") private boolean v2 = true; - @Parameter(names = {"--v3"}, arity = 1, description = "Sign with v3 signature") - private boolean v3 = true; - @Parameter(names = {"--manager"}, description = "Use manager (Cannot work with embedding modules)") private boolean useManager = false; diff --git a/settings.gradle.kts b/settings.gradle.kts index 9f89c95..525b317 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ pluginManagement { plugins { id("com.android.library") version agpVersion id("com.android.application") version agpVersion + id("dev.rikka.tools.refine") version "3.1.1" } }