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 androidMinSdkVersion by extra(28)
|
||||||
val androidTargetSdkVersion by extra(32)
|
val androidTargetSdkVersion by extra(32)
|
||||||
val androidCompileSdkVersion by extra(32)
|
val androidCompileSdkVersion by extra(32)
|
||||||
val androidCompileNdkVersion by extra("23.1.7779620")
|
val androidCompileNdkVersion by extra("24.0.8215888")
|
||||||
val androidBuildToolsVersion by extra("31.0.0")
|
val androidBuildToolsVersion by extra("32.0.0")
|
||||||
val androidSourceCompatibility by extra(JavaVersion.VERSION_11)
|
val androidSourceCompatibility by extra(JavaVersion.VERSION_11)
|
||||||
val androidTargetCompatibility by extra(JavaVersion.VERSION_11)
|
val androidTargetCompatibility by extra(JavaVersion.VERSION_11)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ val coreVerName: String by rootProject.extra
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("dev.rikka.tools.refine")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
}
|
}
|
||||||
|
|
@ -64,10 +65,13 @@ afterEvaluate {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.hiddenapi.bridge)
|
||||||
implementation(projects.patch)
|
implementation(projects.patch)
|
||||||
implementation(projects.services.daemonService)
|
implementation(projects.services.daemonService)
|
||||||
implementation(projects.share.android)
|
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.core:core-ktx:1.7.0")
|
||||||
implementation("androidx.activity:activity-compose:1.6.0-alpha01")
|
implementation("androidx.activity:activity-compose:1.6.0-alpha01")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.1.1")
|
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("com.google.android.material:material:1.5.0")
|
||||||
implementation("dev.rikka.shizuku:api:12.1.0")
|
implementation("dev.rikka.shizuku:api:12.1.0")
|
||||||
implementation("dev.rikka.shizuku:provider: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 {
|
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"
|
const val PREFS_STORAGE_DIRECTORY = "storage_directory"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,39 +3,27 @@ package org.lsposed.lspatch
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import androidx.compose.runtime.getValue
|
import kotlinx.coroutines.Dispatchers
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import androidx.compose.runtime.setValue
|
import org.lsposed.lspatch.util.ShizukuApi
|
||||||
import rikka.shizuku.Shizuku
|
|
||||||
|
|
||||||
const val TAG = "LSPatch Manager"
|
const val TAG = "LSPatch Manager"
|
||||||
|
|
||||||
|
lateinit var lspApp: LSPApplication
|
||||||
|
|
||||||
class LSPApplication : Application() {
|
class LSPApplication : Application() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
var shizukuBinderAvalable = false
|
|
||||||
var shizukuGranted by mutableStateOf(false)
|
|
||||||
|
|
||||||
lateinit var appContext: Context
|
|
||||||
lateinit var prefs: SharedPreferences
|
lateinit var prefs: SharedPreferences
|
||||||
|
|
||||||
init {
|
val globalScope = CoroutineScope(Dispatchers.Default)
|
||||||
Shizuku.addBinderReceivedListenerSticky {
|
|
||||||
shizukuBinderAvalable = true
|
|
||||||
shizukuGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
Shizuku.addBinderDeadListener {
|
|
||||||
shizukuBinderAvalable = false
|
|
||||||
shizukuGranted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
appContext = applicationContext
|
HiddenApiBypass.addHiddenApiExemptions("");
|
||||||
appContext.filesDir.mkdir()
|
lspApp = this
|
||||||
prefs = appContext.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
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 androidx.documentfile.provider.DocumentFile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
|
||||||
import org.lsposed.lspatch.config.MyKeyStore
|
import org.lsposed.lspatch.config.MyKeyStore
|
||||||
import org.lsposed.patch.LSPatch
|
import org.lsposed.patch.LSPatch
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
object Patcher {
|
object Patcher {
|
||||||
|
|
||||||
class Options(
|
class Options(
|
||||||
private val apkPaths: List<String>,
|
private val apkPaths: List<String>,
|
||||||
private val debuggable: Boolean,
|
private val debuggable: Boolean,
|
||||||
private val sigbypassLevel: Int,
|
private val sigbypassLevel: Int,
|
||||||
private val v1: Boolean,
|
private val v1: Boolean,
|
||||||
private val v2: Boolean,
|
private val v2: Boolean,
|
||||||
private val v3: Boolean,
|
|
||||||
private val useManager: Boolean,
|
private val useManager: Boolean,
|
||||||
private val overrideVersionCode: Boolean,
|
private val overrideVersionCode: Boolean,
|
||||||
private val verbose: Boolean,
|
private val verbose: Boolean,
|
||||||
private val embeddedModules: List<String>?
|
private val embeddedModules: List<String>?
|
||||||
) {
|
) {
|
||||||
lateinit var outputPath: String
|
lateinit var outputDir: File
|
||||||
|
|
||||||
fun toStringArray(): Array<String> {
|
fun toStringArray(): Array<String> {
|
||||||
return buildList {
|
return buildList {
|
||||||
add("-f")
|
|
||||||
addAll(apkPaths)
|
addAll(apkPaths)
|
||||||
add("-o"); add(outputPath)
|
add("-o"); add(outputDir.absolutePath)
|
||||||
if (debuggable) add("-d")
|
if (debuggable) add("-d")
|
||||||
add("-l"); add(sigbypassLevel.toString())
|
add("-l"); add(sigbypassLevel.toString())
|
||||||
add("--v1"); add(v1.toString())
|
add("--v1"); add(v1.toString())
|
||||||
add("--v2"); add(v2.toString())
|
add("--v2"); add(v2.toString())
|
||||||
add("--v3"); add(v3.toString())
|
|
||||||
if (useManager) add("--manager")
|
if (useManager) add("--manager")
|
||||||
if (overrideVersionCode) add("-r")
|
if (overrideVersionCode) add("-r")
|
||||||
if (verbose) add("-v")
|
if (verbose) add("-v")
|
||||||
|
|
@ -52,24 +51,27 @@ object Patcher {
|
||||||
|
|
||||||
suspend fun patch(context: Context, logger: Logger, options: Options) {
|
suspend fun patch(context: Context, logger: Logger, options: Options) {
|
||||||
withContext(Dispatchers.IO) {
|
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()
|
LSPatch(logger, *options.toStringArray()).doCommandLine()
|
||||||
|
|
||||||
val uri = LSPApplication.prefs.getString(Constants.PREFS_STORAGE_DIRECTORY, null)?.toUri()
|
val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri()
|
||||||
?: throw IllegalStateException("Uri is null")
|
?: throw IOException("Uri is null")
|
||||||
val root = DocumentFile.fromTreeUri(context, uri)
|
val root = DocumentFile.fromTreeUri(context, uri)
|
||||||
?: throw IllegalStateException("DocumentFile is null")
|
?: throw IOException("DocumentFile is null")
|
||||||
root.listFiles().forEach { it.delete() }
|
root.listFiles().forEach {
|
||||||
File(options.outputPath)
|
if (it.name?.endsWith("-lspatched.apk") == true) it.delete()
|
||||||
|
}
|
||||||
|
options.outputDir
|
||||||
.walk()
|
.walk()
|
||||||
.filter { it.isFile }
|
.filter { it.isFile }
|
||||||
.forEach {
|
.forEach { apk ->
|
||||||
val file = root.createFile("application/vnd.android.package-archive", it.name)
|
val file = root.createFile("application/vnd.android.package-archive", apk.name)
|
||||||
?: throw IllegalStateException("Failed to create output file")
|
?: throw IOException("Failed to create output file")
|
||||||
val os = context.contentResolver.openOutputStream(file.uri)
|
val output = context.contentResolver.openOutputStream(file.uri)
|
||||||
?: throw IllegalStateException("Failed to open output stream")
|
?: throw IOException("Failed to open output stream")
|
||||||
os.use { output ->
|
output.use {
|
||||||
it.inputStream().use { input ->
|
apk.inputStream().use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,26 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.lsposed.lspatch.LSPApplication.Companion.appContext
|
import org.lsposed.lspatch.Constants.PREFS_KEYSTORE_ALIAS
|
||||||
import org.lsposed.lspatch.LSPApplication.Companion.prefs
|
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
|
import java.io.File
|
||||||
|
|
||||||
object MyKeyStore {
|
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
|
val password: String
|
||||||
get() = prefs.getString("keystore_password", "123456")!!
|
get() = lspApp.prefs.getString("keystore_password", "123456")!!
|
||||||
|
|
||||||
val alias: String
|
val alias: String
|
||||||
get() = prefs.getString("keystore_alias", "key0")!!
|
get() = lspApp.prefs.getString("keystore_alias", "key0")!!
|
||||||
|
|
||||||
val aliasPassword: String
|
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())
|
private var mUseDefault by mutableStateOf(!file.exists())
|
||||||
val useDefault by derivedStateOf { mUseDefault }
|
val useDefault by derivedStateOf { mUseDefault }
|
||||||
|
|
@ -31,10 +33,10 @@ object MyKeyStore {
|
||||||
suspend fun reset() {
|
suspend fun reset() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
file.delete()
|
file.delete()
|
||||||
prefs.edit()
|
lspApp.prefs.edit()
|
||||||
.putString("keystore_password", "123456")
|
.putString(PREFS_KEYSTORE_PASSWORD, "123456")
|
||||||
.putString("keystore_alias", "key0")
|
.putString(PREFS_KEYSTORE_ALIAS, "key0")
|
||||||
.putString("keystore_alias_password", "123456")
|
.putString(PREFS_KEYSTORE_ALIAS_PASSWORD, "123456")
|
||||||
.apply()
|
.apply()
|
||||||
mUseDefault = true
|
mUseDefault = true
|
||||||
}
|
}
|
||||||
|
|
@ -43,10 +45,10 @@ object MyKeyStore {
|
||||||
suspend fun setCustom(password: String, alias: String, aliasPassword: String) {
|
suspend fun setCustom(password: String, alias: String, aliasPassword: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
tmpFile.renameTo(file)
|
tmpFile.renameTo(file)
|
||||||
prefs.edit()
|
lspApp.prefs.edit()
|
||||||
.putString("keystore_password", password)
|
.putString(PREFS_KEYSTORE_PASSWORD, password)
|
||||||
.putString("keystore_alias", alias)
|
.putString(PREFS_KEYSTORE_ALIAS, alias)
|
||||||
.putString("keystore_alias_password", aliasPassword)
|
.putString(PREFS_KEYSTORE_ALIAS_PASSWORD, aliasPassword)
|
||||||
.apply()
|
.apply()
|
||||||
mUseDefault = false
|
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.page.PageList
|
||||||
import org.lsposed.lspatch.ui.theme.LSPTheme
|
import org.lsposed.lspatch.ui.theme.LSPTheme
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||||
|
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
|
||||||
import org.lsposed.lspatch.ui.util.currentRoute
|
import org.lsposed.lspatch.ui.util.currentRoute
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
@ -27,11 +28,14 @@ class MainActivity : ComponentActivity() {
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberAnimatedNavController()
|
val navController = rememberAnimatedNavController()
|
||||||
val currentRoute = navController.currentRoute
|
val currentRoute = navController.currentRoute
|
||||||
val currentPage = if (currentRoute == null) null else PageList.valueOf(currentRoute.substringBefore('/'))
|
|
||||||
var mainPage by rememberSaveable { mutableStateOf(PageList.Home) }
|
var mainPage by rememberSaveable { mutableStateOf(PageList.Home) }
|
||||||
|
|
||||||
LSPTheme {
|
LSPTheme {
|
||||||
CompositionLocalProvider(LocalNavController provides navController) {
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalNavController provides navController,
|
||||||
|
LocalSnackbarHost provides snackbarHostState
|
||||||
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
MainNavigationBar(mainPage) {
|
MainNavigationBar(mainPage) {
|
||||||
|
|
@ -42,7 +46,8 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
MainNavHost(navController, Modifier.padding(innerPadding))
|
MainNavHost(navController, Modifier.padding(innerPadding))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,19 +24,16 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.lspatch.BuildConfig
|
import org.lsposed.lspatch.BuildConfig
|
||||||
import org.lsposed.lspatch.LSPApplication
|
|
||||||
import org.lsposed.lspatch.R
|
import org.lsposed.lspatch.R
|
||||||
import org.lsposed.lspatch.ui.util.HtmlText
|
import org.lsposed.lspatch.ui.util.HtmlText
|
||||||
|
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
|
||||||
|
import org.lsposed.lspatch.util.ShizukuApi
|
||||||
import rikka.shizuku.Shizuku
|
import rikka.shizuku.Shizuku
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage() {
|
fun HomePage() {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
Scaffold(topBar = { TopBar() }) { innerPadding ->
|
||||||
Scaffold(
|
|
||||||
topBar = { TopBar() },
|
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
|
||||||
) { innerPadding ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
|
|
@ -45,7 +42,7 @@ fun HomePage() {
|
||||||
) {
|
) {
|
||||||
ShizukuCard()
|
ShizukuCard()
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
InfoCard(snackbarHostState)
|
InfoCard()
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
SupportCard()
|
SupportCard()
|
||||||
}
|
}
|
||||||
|
|
@ -64,12 +61,12 @@ private fun TopBar() {
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val listener: (Int, Int) -> Unit = { _, grantResult ->
|
private val listener: (Int, Int) -> Unit = { _, grantResult ->
|
||||||
LSPApplication.shizukuGranted = grantResult == PackageManager.PERMISSION_GRANTED
|
ShizukuApi.isPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -87,12 +84,12 @@ private fun ShizukuCard() {
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (LSPApplication.shizukuBinderAvalable && !LSPApplication.shizukuGranted) {
|
if (ShizukuApi.isBinderAvalable && !ShizukuApi.isPermissionGranted) {
|
||||||
Shizuku.requestPermission(114514)
|
Shizuku.requestPermission(114514)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = run {
|
containerColor = run {
|
||||||
if (LSPApplication.shizukuGranted) MaterialTheme.colorScheme.secondaryContainer
|
if (ShizukuApi.isPermissionGranted) MaterialTheme.colorScheme.secondaryContainer
|
||||||
else MaterialTheme.colorScheme.errorContainer
|
else MaterialTheme.colorScheme.errorContainer
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -102,11 +99,11 @@ private fun ShizukuCard() {
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (LSPApplication.shizukuGranted) {
|
if (ShizukuApi.isPermissionGranted) {
|
||||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_shizuku_available))
|
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.shizuku_available))
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
Column(Modifier.padding(start = 20.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.home_shizuku_available),
|
text = stringResource(R.string.shizuku_available),
|
||||||
fontFamily = FontFamily.Serif,
|
fontFamily = FontFamily.Serif,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
|
@ -117,10 +114,10 @@ private fun ShizukuCard() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)) {
|
Column(Modifier.padding(start = 20.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.home_shizuku_unavailable),
|
text = stringResource(R.string.shizuku_unavailable),
|
||||||
fontFamily = FontFamily.Serif,
|
fontFamily = FontFamily.Serif,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
|
@ -151,8 +148,9 @@ private val device = buildString {
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun InfoCard(snackbarHostState: SnackbarHostState) {
|
private fun InfoCard() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val snackbarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
ElevatedCard {
|
ElevatedCard {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -190,7 +188,7 @@ private fun InfoCard(snackbarHostState: SnackbarHostState) {
|
||||||
onClick = {
|
onClick = {
|
||||||
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", contents.toString()))
|
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", contents.toString()))
|
||||||
scope.launch { snackbarHostState.showSnackbar(copiedMessage) }
|
scope.launch { snackbarHost.showSnackbar(copiedMessage) }
|
||||||
},
|
},
|
||||||
content = { Text(stringResource(android.R.string.copy)) }
|
content = { Text(stringResource(android.R.string.copy)) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,31 @@ import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.lspatch.Constants
|
import org.lsposed.lspatch.*
|
||||||
import org.lsposed.lspatch.LSPApplication
|
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
|
||||||
import org.lsposed.lspatch.R
|
import org.lsposed.lspatch.R
|
||||||
import org.lsposed.lspatch.TAG
|
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||||
|
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ManagePage() {
|
fun ManagePage() {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopBar() },
|
topBar = { TopBar() },
|
||||||
floatingActionButton = { Fab(snackbarHostState) },
|
floatingActionButton = { Fab() }
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -41,8 +43,9 @@ private fun TopBar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Fab(snackbarHostState: SnackbarHostState) {
|
private fun Fab() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val snackbarHost = LocalSnackbarHost.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var shouldSelectDirectory by remember { mutableStateOf(false) }
|
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 uri = it.data?.data ?: throw IOException("No data")
|
||||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
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}")
|
Log.i(TAG, "Storage directory: ${uri.path}")
|
||||||
navController.navigate(PageList.NewPatch.name)
|
navController.navigate(PageList.NewPatch.name)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error when requesting saving directory", e)
|
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 }
|
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)) }
|
text = { Text(stringResource(R.string.patch_select_dir_text)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -89,17 +98,19 @@ private fun Fab(snackbarHostState: SnackbarHostState) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },
|
content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },
|
||||||
onClick = {
|
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) {
|
if (uri == null) {
|
||||||
shouldSelectDirectory = true
|
shouldSelectDirectory = true
|
||||||
} else {
|
} else {
|
||||||
try {
|
runCatching {
|
||||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted")
|
||||||
|
}.onSuccess {
|
||||||
navController.navigate(PageList.NewPatch.name)
|
navController.navigate(PageList.NewPatch.name)
|
||||||
} catch (e: SecurityException) {
|
}.onFailure {
|
||||||
Log.e(TAG, "Failed to take persistable permission for saved uri", e)
|
Log.w(TAG, "Failed to take persistable permission for saved uri", it)
|
||||||
LSPApplication.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, null).apply()
|
lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, null).apply()
|
||||||
shouldSelectDirectory = true
|
shouldSelectDirectory = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package org.lsposed.lspatch.ui.page
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
|
|
@ -25,81 +26,71 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.lspatch.Patcher
|
import org.lsposed.lspatch.Patcher
|
||||||
import org.lsposed.lspatch.R
|
import org.lsposed.lspatch.R
|
||||||
import org.lsposed.lspatch.TAG
|
import org.lsposed.lspatch.TAG
|
||||||
|
import org.lsposed.lspatch.lspApp
|
||||||
import org.lsposed.lspatch.ui.component.SelectionColumn
|
import org.lsposed.lspatch.ui.component.SelectionColumn
|
||||||
import org.lsposed.lspatch.ui.component.ShimmerAnimation
|
import org.lsposed.lspatch.ui.component.ShimmerAnimation
|
||||||
import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
|
import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
|
||||||
import org.lsposed.lspatch.ui.component.settings.SettingsItem
|
import org.lsposed.lspatch.ui.component.settings.SettingsItem
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
import org.lsposed.lspatch.ui.util.*
|
||||||
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.viewmodel.AppInfo
|
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
||||||
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
|
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
|
||||||
|
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
|
||||||
|
import org.lsposed.lspatch.util.LSPPackageInstaller
|
||||||
|
import org.lsposed.lspatch.util.ShizukuApi
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private enum class PatchState {
|
|
||||||
SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED, ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewPatchPage(entry: NavBackStackEntry) {
|
fun NewPatchPage(entry: NavBackStackEntry) {
|
||||||
|
val viewModel = viewModel<NewPatchViewModel>()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val patchApp by entry.observeState<AppInfo>("appInfo")
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val isCancelled by entry.observeState<Boolean>("isCancelled")
|
val isCancelled by entry.observeState<Boolean>("isCancelled")
|
||||||
var patchState by rememberSaveable { mutableStateOf(PatchState.SELECTING) }
|
entry.savedStateHandle.getLiveData<AppInfo>("appInfo").observe(lifecycleOwner) {
|
||||||
var patchOptions by rememberSaveable { mutableStateOf<Patcher.Options?>(null) }
|
viewModel.patchApp = it
|
||||||
if (patchState == PatchState.SELECTING) {
|
|
||||||
when {
|
|
||||||
isCancelled == true -> {
|
|
||||||
LaunchedEffect(entry) { navController.popBackStack() }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
patchApp != null -> patchState = PatchState.CONFIGURING
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "NewPatchPage: $patchState")
|
Log.d(TAG, "NewPatchPage: ${viewModel.patchState}")
|
||||||
if (patchState == PatchState.SELECTING) {
|
if (viewModel.patchState == PatchState.SELECTING) {
|
||||||
LaunchedEffect(entry) {
|
when {
|
||||||
navController.navigate(PageList.SelectApps.name + "/false")
|
isCancelled == true -> {
|
||||||
|
LaunchedEffect(viewModel) { navController.popBackStack() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.patchApp != null -> {
|
||||||
|
LaunchedEffect(viewModel) { viewModel.configurePatch() }
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LaunchedEffect(viewModel) { navController.navigate(PageList.SelectApps.name + "/false") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopBar(patchApp!!) },
|
topBar = { TopBar(viewModel.patchApp!!) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (patchState == PatchState.CONFIGURING) {
|
if (viewModel.patchState == PatchState.CONFIGURING) {
|
||||||
ConfiguringFab { patchState = PatchState.SUBMITTING }
|
ConfiguringFab()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
if (patchState == PatchState.CONFIGURING || patchState == PatchState.SUBMITTING) {
|
if (viewModel.patchState == PatchState.CONFIGURING) {
|
||||||
PatchOptionsBody(
|
entry.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected", SnapshotStateList()).observe(lifecycleOwner) {
|
||||||
modifier = Modifier.padding(innerPadding),
|
viewModel.embeddedModules = it
|
||||||
patchState = patchState,
|
|
||||||
patchApp = patchApp!!,
|
|
||||||
onSubmit = {
|
|
||||||
patchOptions = it
|
|
||||||
patchState = PatchState.PATCHING
|
|
||||||
}
|
}
|
||||||
)
|
PatchOptionsBody(Modifier.padding(innerPadding))
|
||||||
} else {
|
} else {
|
||||||
DoPatchBody(
|
DoPatchBody(Modifier.padding(innerPadding))
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
patchState = patchState,
|
|
||||||
patchOptions = patchOptions!!,
|
|
||||||
onFinish = { patchState = PatchState.FINISHED },
|
|
||||||
onFail = { patchState = PatchState.ERROR }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,11 +117,12 @@ private fun TopBar(patchApp: AppInfo) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConfiguringFab(onClick: () -> Unit) {
|
private fun ConfiguringFab() {
|
||||||
|
val viewModel = viewModel<NewPatchViewModel>()
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.patch_start)) },
|
text = { Text(stringResource(R.string.patch_start)) },
|
||||||
icon = { Icon(Icons.Outlined.AutoFixHigh, null) },
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PatchOptionsBody(
|
private fun PatchOptionsBody(modifier: Modifier) {
|
||||||
modifier: Modifier,
|
|
||||||
patchState: PatchState,
|
|
||||||
patchApp: AppInfo,
|
|
||||||
onSubmit: (Patcher.Options) -> Unit
|
|
||||||
) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val viewModel = viewModel<NewPatchViewModel>()
|
val viewModel = viewModel<NewPatchViewModel>()
|
||||||
val embeddedModules = navController.currentBackStackEntry!!
|
val navController = LocalNavController.current
|
||||||
.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier.verticalScroll(rememberScrollState())) {
|
Column(modifier.verticalScroll(rememberScrollState())) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -194,10 +164,9 @@ private fun PatchOptionsBody(
|
||||||
desc = stringResource(R.string.patch_portable_desc),
|
desc = stringResource(R.string.patch_portable_desc),
|
||||||
extraContent = {
|
extraContent = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { navController.navigate(PageList.SelectApps.name + "/true") }
|
onClick = { navController.navigate(PageList.SelectApps.name + "/true") },
|
||||||
) {
|
content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) }
|
||||||
Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -265,19 +234,7 @@ private fun PatchOptionsBody(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private class PatchLogger(private val logs: MutableList<Pair<Int, String>>) : Logger() {
|
||||||
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) {
|
override fun d(msg: String) {
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
|
|
@ -294,25 +251,34 @@ private fun DoPatchBody(
|
||||||
Log.e(TAG, msg)
|
Log.e(TAG, msg)
|
||||||
logs += Log.ERROR to msg
|
logs += Log.ERROR to msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(patchOptions) {
|
@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 {
|
try {
|
||||||
Patcher.patch(context, logger, patchOptions)
|
Patcher.patch(context, logger, viewModel.patchOptions)
|
||||||
onFinish()
|
viewModel.finishPatch()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logger.e(t.message.orEmpty())
|
logger.e(t.message.orEmpty())
|
||||||
logger.e(t.stackTraceToString())
|
logger.e(t.stackTraceToString())
|
||||||
onFail()
|
viewModel.failPatch()
|
||||||
} finally {
|
} finally {
|
||||||
File(patchOptions.outputPath).deleteRecursively()
|
viewModel.patchOptions.outputDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxWithConstraints(modifier.padding(24.dp)) {
|
BoxWithConstraints(modifier.padding(24.dp)) {
|
||||||
val shellBoxMaxHeight =
|
val shellBoxMaxHeight =
|
||||||
if (patchState == PatchState.PATCHING) maxHeight
|
if (viewModel.patchState == PatchState.PATCHING) maxHeight
|
||||||
else maxHeight - ButtonDefaults.MinHeight - 12.dp
|
else maxHeight - ButtonDefaults.MinHeight - 12.dp
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -320,7 +286,7 @@ private fun DoPatchBody(
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.animateContentSize(spring(stiffness = Spring.StiffnessLow))
|
.animateContentSize(spring(stiffness = Spring.StiffnessLow))
|
||||||
) {
|
) {
|
||||||
ShimmerAnimation(enabled = patchState == PatchState.PATCHING) {
|
ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
|
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
|
||||||
) {
|
) {
|
||||||
|
|
@ -351,38 +317,130 @@ private fun DoPatchBody(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchState == PatchState.FINISHED) {
|
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)) {
|
Row(Modifier.padding(top = 12.dp)) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.popBackStack() },
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
content = { Text(stringResource(R.string.patch_return)) }
|
content = { Text(stringResource(R.string.patch_return)) }
|
||||||
)
|
)
|
||||||
Spacer(Modifier.weight(0.2f))
|
Spacer(Modifier.weight(0.2f))
|
||||||
Button(
|
Button(
|
||||||
onClick = { /* TODO: Install */ },
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = {
|
||||||
|
if (!ShizukuApi.isPermissionGranted) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(shizukuUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installing = true
|
||||||
|
},
|
||||||
content = { Text(stringResource(R.string.patch_install)) }
|
content = { Text(stringResource(R.string.patch_install)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (patchState == PatchState.ERROR) {
|
}
|
||||||
|
PatchState.ERROR -> {
|
||||||
Row(Modifier.padding(top = 12.dp)) {
|
Row(Modifier.padding(top = 12.dp)) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.popBackStack() },
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
content = { Text(stringResource(R.string.patch_return)) }
|
content = { Text(stringResource(R.string.patch_return)) }
|
||||||
)
|
)
|
||||||
Spacer(Modifier.weight(0.2f))
|
Spacer(Modifier.weight(0.2f))
|
||||||
Button(
|
Button(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
onClick = {
|
onClick = {
|
||||||
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", logs.joinToString { it.second + "\n" }))
|
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", logs.joinToString { it.second + "\n" }))
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
content = { Text(stringResource(R.string.patch_copy_error)) }
|
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.interaction.PressInteraction
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
@ -19,6 +20,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.lspatch.R
|
import org.lsposed.lspatch.R
|
||||||
|
|
@ -150,7 +152,13 @@ private fun KeyStore() {
|
||||||
onClick = { dropDownExpanded = false; showDialog = false }
|
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 = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
@ -163,7 +171,7 @@ private fun KeyStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val wrongText = when {
|
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)
|
wrongAliasName -> stringResource(R.string.settings_keystore_wrong_alias)
|
||||||
wrongPassword -> stringResource(R.string.settings_keystore_wrong_password)
|
wrongPassword -> stringResource(R.string.settings_keystore_wrong_password)
|
||||||
wrongKeystore -> stringResource(R.string.settings_keystore_wrong_keystore)
|
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
|
package org.lsposed.lspatch.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
val LocalNavController = compositionLocalOf<NavHostController> {
|
|
||||||
error("CompositionLocal LocalNavController not present")
|
|
||||||
}
|
|
||||||
|
|
||||||
val NavController.currentRoute: String?
|
val NavController.currentRoute: String?
|
||||||
@Composable get() = currentBackStackEntryAsState().value?.destination?.route
|
@Composable get() = currentBackStackEntryAsState().value?.destination?.route
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,53 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.lsposed.lspatch.Patcher
|
||||||
|
|
||||||
class NewPatchViewModel : ViewModel() {
|
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 useManager by mutableStateOf(true)
|
||||||
var debuggable by mutableStateOf(false)
|
var debuggable by mutableStateOf(false)
|
||||||
var overrideVersionCode by mutableStateOf(false)
|
var overrideVersionCode by mutableStateOf(false)
|
||||||
var sign = mutableStateListOf(false, true, true)
|
var sign = mutableStateListOf(false, true)
|
||||||
var sigBypassLevel by mutableStateOf(2)
|
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>
|
<resources>
|
||||||
<string name="add">Add</string>
|
<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_repo">Repo</string>
|
||||||
<string name="page_logs">Logs</string>
|
<string name="page_logs">Logs</string>
|
||||||
|
|
||||||
<!-- Home Page -->
|
<!-- 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_shizuku_warning">Some functions unavailable</string>
|
||||||
<string name="home_api_version">API Version</string>
|
<string name="home_api_version">API Version</string>
|
||||||
<string name="home_lspatch_version">LSPatch Version</string>
|
<string name="home_lspatch_version">LSPatch Version</string>
|
||||||
|
|
@ -44,6 +44,11 @@
|
||||||
<string name="patch_start">Start Patch</string>
|
<string name="patch_start">Start Patch</string>
|
||||||
<string name="patch_return">Return</string>
|
<string name="patch_return">Return</string>
|
||||||
<string name="patch_install">Install</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>
|
<string name="patch_copy_error">Copy error</string>
|
||||||
|
|
||||||
<!-- Select Apps Page -->
|
<!-- Select Apps Page -->
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,6 @@ public class LSPatch {
|
||||||
@Parameter(names = {"--v2"}, arity = 1, description = "Sign with v2 signature")
|
@Parameter(names = {"--v2"}, arity = 1, description = "Sign with v2 signature")
|
||||||
private boolean v2 = true;
|
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)")
|
@Parameter(names = {"--manager"}, description = "Use manager (Cannot work with embedding modules)")
|
||||||
private boolean useManager = false;
|
private boolean useManager = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.library") version agpVersion
|
id("com.android.library") version agpVersion
|
||||||
id("com.android.application") version agpVersion
|
id("com.android.application") version agpVersion
|
||||||
|
id("dev.rikka.tools.refine") version "3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue