Implement install patched app & Refactor UI
This commit is contained in:
parent
f5d2c20a37
commit
5d3f0ec9f7
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
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<String>?
|
||||
) {
|
||||
lateinit var outputPath: String
|
||||
lateinit var outputDir: File
|
||||
|
||||
fun toStringArray(): Array<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NewPatchViewModel>()
|
||||
val navController = LocalNavController.current
|
||||
val patchApp by entry.observeState<AppInfo>("appInfo")
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val isCancelled by entry.observeState<Boolean>("isCancelled")
|
||||
var patchState by rememberSaveable { mutableStateOf(PatchState.SELECTING) }
|
||||
var patchOptions by rememberSaveable { mutableStateOf<Patcher.Options?>(null) }
|
||||
if (patchState == PatchState.SELECTING) {
|
||||
when {
|
||||
isCancelled == true -> {
|
||||
LaunchedEffect(entry) { navController.popBackStack() }
|
||||
return
|
||||
}
|
||||
patchApp != null -> patchState = PatchState.CONFIGURING
|
||||
}
|
||||
entry.savedStateHandle.getLiveData<AppInfo>("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<SnapshotStateList<AppInfo>>("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<NewPatchViewModel>()
|
||||
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<NewPatchViewModel>()
|
||||
val embeddedModules = navController.currentBackStackEntry!!
|
||||
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("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<Pair<Int, String>>() }
|
||||
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<Pair<Int, String>>) : 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<NewPatchViewModel>()
|
||||
val context = LocalContext.current
|
||||
val snackbarHost = LocalSnackbarHost.current
|
||||
val navController = LocalNavController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val logs = remember { mutableStateListOf<Pair<Int, String>>() }
|
||||
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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<NavHostController> {
|
||||
error("CompositionLocal LocalNavController not present")
|
||||
}
|
||||
|
||||
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
|
||||
error("CompositionLocal LocalSnackbarController not present")
|
||||
}
|
||||
|
|
@ -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<NavHostController> {
|
||||
error("CompositionLocal LocalNavController not present")
|
||||
}
|
||||
|
||||
val NavController.currentRoute: String?
|
||||
@Composable get() = currentBackStackEntryAsState().value?.destination?.route
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AppInfo?>(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<AppInfo>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int, String?> {
|
||||
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<Unit> { 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<Int, String?> {
|
||||
var status = PackageInstaller.STATUS_FAILURE
|
||||
var message: String? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
var result: Intent? = null
|
||||
suspendCoroutine<Unit> { 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<resources>
|
||||
<string name="add">Add</string>
|
||||
<string name="shizuku_available">Shizuku service available</string>
|
||||
<string name="shizuku_unavailable">Shizuku service not connected</string>
|
||||
|
||||
<string name="page_repo">Repo</string>
|
||||
<string name="page_logs">Logs</string>
|
||||
|
||||
<!-- Home Page -->
|
||||
<string name="home_shizuku_available">Shizuku service available</string>
|
||||
<string name="home_shizuku_unavailable">Shizuku service not connected</string>
|
||||
<string name="home_shizuku_warning">Some functions unavailable</string>
|
||||
<string name="home_api_version">API Version</string>
|
||||
<string name="home_lspatch_version">LSPatch Version</string>
|
||||
|
|
@ -44,6 +44,11 @@
|
|||
<string name="patch_start">Start Patch</string>
|
||||
<string name="patch_return">Return</string>
|
||||
<string name="patch_install">Install</string>
|
||||
<string name="patch_installing">Installing</string>
|
||||
<string name="patch_uninstall">Uninstall</string>
|
||||
<string name="patch_uninstall_text">Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data.</string>
|
||||
<string name="patch_install_successfully">Install successfully</string>
|
||||
<string name="patch_install_failed">Install failed</string>
|
||||
<string name="patch_copy_error">Copy error</string>
|
||||
|
||||
<!-- Select Apps Page -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue