Support module scope

This commit is contained in:
Nullptr 2022-05-30 16:20:18 +08:00
parent 66804c425d
commit 290989b0e6
No known key found for this signature in database
GPG Key ID: 0B9D02052FF536BD
20 changed files with 321 additions and 105 deletions

View File

@ -7,6 +7,7 @@ val coreVerName: String by rootProject.extra
plugins { plugins {
id("com.android.application") id("com.android.application")
id("com.google.devtools.ksp")
id("dev.rikka.tools.refine") id("dev.rikka.tools.refine")
id("kotlin-parcelize") id("kotlin-parcelize")
kotlin("android") kotlin("android")
@ -73,6 +74,8 @@ dependencies {
implementation(projects.share.android) implementation(projects.share.android)
implementation(projects.share.java) implementation(projects.share.java)
val roomVersion = "2.4.2"
annotationProcessor("androidx.room:room-compiler:$roomVersion")
compileOnly("dev.rikka.hidden:stub:2.3.1") compileOnly("dev.rikka.hidden:stub:2.3.1")
implementation("dev.rikka.hidden:compat: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")
@ -85,6 +88,8 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-rc01") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-rc01")
implementation("androidx.navigation:navigation-compose:2.5.0-rc01") implementation("androidx.navigation:navigation-compose:2.5.0-rc01")
implementation("androidx.preference:preference:1.2.0") implementation("androidx.preference:preference:1.2.0")
implementation("androidx.room:room-ktx:$roomVersion")
implementation("androidx.room:room-runtime:$roomVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:0.24.9-beta") implementation("com.google.accompanist:accompanist-drawablepainter:0.24.9-beta")
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.9-beta") implementation("com.google.accompanist:accompanist-navigation-animation:0.24.9-beta")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.9-beta") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.9-beta")
@ -93,4 +98,5 @@ dependencies {
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") implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
ksp("androidx.room:room-compiler:$roomVersion")
} }

View File

@ -5,7 +5,9 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.lspatch.util.ShizukuApi
import java.io.File import java.io.File
@ -29,5 +31,6 @@ class LSPApplication : Application() {
tmpApkDir.mkdirs() tmpApkDir.mkdirs()
prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE) prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE)
ShizukuApi.init() ShizukuApi.init()
globalScope.launch { LSPPackageManager.fetchAppList() }
} }
} }

View File

@ -0,0 +1,72 @@
package org.lsposed.lspatch.config
import androidx.room.Room
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.withContext
import org.lsposed.lspatch.database.LSPDatabase
import org.lsposed.lspatch.database.entity.Module
import org.lsposed.lspatch.database.entity.Scope
import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.util.ModuleLoader
object ConfigManager {
@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
private val db: LSPDatabase = Room.databaseBuilder(
lspApp, LSPDatabase::class.java, "modules_config.db"
).build()
private val moduleDao = db.moduleDao()
private val scopeDao = db.scopeDao()
private val loadedModules = mutableMapOf<Module, org.lsposed.lspd.models.Module>()
suspend fun updateModules(newModules: Map<String, String>) =
withContext(dispatcher) {
for (module in moduleDao.getAll()) {
val apkPath = newModules[module.pkgName]
if (apkPath == null) {
moduleDao.delete(module)
loadedModules.remove(module)
} else if (module.apkPath != apkPath) {
module.apkPath = apkPath
loadedModules.remove(module)
}
}
for ((pkgName, apkPath) in newModules) {
moduleDao.insert(Module(pkgName, apkPath))
}
}
suspend fun activateModule(pkgName: String, module: Module) =
withContext(dispatcher) {
scopeDao.insert(Scope(appPkgName = pkgName, modulePkgName = module.pkgName))
}
suspend fun deactivateModule(pkgName: String, module: Module) =
withContext(dispatcher) {
scopeDao.delete(Scope(appPkgName = pkgName, modulePkgName = module.pkgName))
}
suspend fun getModulesForApp(pkgName: String): List<Module> =
withContext(dispatcher) {
return@withContext scopeDao.getModulesForApp(pkgName)
}
suspend fun getModuleFilesForApp(pkgName: String): List<org.lsposed.lspd.models.Module> =
withContext(dispatcher) {
val modules = scopeDao.getModulesForApp(pkgName)
return@withContext modules.map {
loadedModules.getOrPut(it) {
org.lsposed.lspd.models.Module().apply {
packageName = it.pkgName
apkPath = it.apkPath
file = ModuleLoader.loadModule(it.apkPath)
}
}
}
}
}

View File

@ -0,0 +1,15 @@
package org.lsposed.lspatch.database
import androidx.room.Database
import androidx.room.RoomDatabase
import org.lsposed.lspatch.database.dao.ModuleDao
import org.lsposed.lspatch.database.dao.ScopeDao
import org.lsposed.lspatch.database.entity.Module
import org.lsposed.lspatch.database.entity.Scope
@Database(entities = [Module::class, Scope::class], version = 1)
abstract class LSPDatabase : RoomDatabase() {
abstract fun moduleDao(): ModuleDao
abstract fun scopeDao(): ScopeDao
}

View File

@ -0,0 +1,21 @@
package org.lsposed.lspatch.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import org.lsposed.lspatch.database.entity.Module
@Dao
interface ModuleDao {
@Query("SELECT * FROM module")
suspend fun getAll(): List<Module>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(module: Module)
@Delete
suspend fun delete(module: Module)
}

View File

@ -0,0 +1,21 @@
package org.lsposed.lspatch.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import org.lsposed.lspatch.database.entity.Module
import org.lsposed.lspatch.database.entity.Scope
@Dao
interface ScopeDao {
@Query("SELECT * FROM module INNER JOIN scope ON module.pkgName = scope.modulePkgName WHERE scope.appPkgName = :appPkgName")
suspend fun getModulesForApp(appPkgName: String): List<Module>
@Insert
suspend fun insert(scope: Scope)
@Delete
suspend fun delete(scope: Scope)
}

View File

@ -0,0 +1,10 @@
package org.lsposed.lspatch.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Module(
@PrimaryKey val pkgName: String,
var apkPath: String
)

View File

@ -0,0 +1,13 @@
package org.lsposed.lspatch.database.entity
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
primaryKeys = ["appPkgName", "modulePkgName"],
foreignKeys = [ForeignKey(entity = Module::class, parentColumns = ["pkgName"], childColumns = ["modulePkgName"], onDelete = ForeignKey.CASCADE)]
)
data class Scope(
val appPkgName: String,
val modulePkgName: String
)

View File

@ -1,19 +1,31 @@
package org.lsposed.lspatch.manager package org.lsposed.lspatch.manager
import android.os.Binder
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log
import kotlinx.coroutines.runBlocking
import org.lsposed.lspatch.config.ConfigManager
import org.lsposed.lspatch.lspApp
import org.lsposed.lspd.models.Module import org.lsposed.lspd.models.Module
import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.service.ILSPApplicationService
object ManagerService : ILSPApplicationService.Stub() { object ManagerService : ILSPApplicationService.Stub() {
private const val TAG = "ManagerService"
override fun requestModuleBinder(name: String): IBinder { override fun requestModuleBinder(name: String): IBinder {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun getModulesList(): List<Module> { override fun getModulesList(): List<Module> {
return ModuleProvider.allModules val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid())
val list = app?.let {
runBlocking { ConfigManager.getModuleFilesForApp(it) }
}.orEmpty()
Log.d(TAG, "$app calls getModulesList: $list")
return list
} }
override fun getPrefsPath(packageName: String): String { override fun getPrefsPath(packageName: String): String {

View File

@ -2,55 +2,31 @@ package org.lsposed.lspatch.manager
import android.content.ContentProvider import android.content.ContentProvider
import android.content.ContentValues import android.content.ContentValues
import android.content.pm.PackageManager
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import org.lsposed.lspatch.TAG import org.lsposed.lspatch.TAG
import org.lsposed.lspatch.util.ModuleLoader import org.lsposed.lspatch.lspApp
import org.lsposed.lspd.models.Module
class ModuleProvider : ContentProvider() { class ModuleProvider : ContentProvider() {
companion object {
lateinit var allModules: List<Module>
}
override fun onCreate(): Boolean { override fun onCreate(): Boolean {
return false return false
} }
override fun call(method: String, arg: String?, extras: Bundle?): Bundle { override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
val app = context!!.packageManager.getNameForUid(Binder.getCallingUid()) val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid())
Log.d(TAG, "$app calls binder") Log.d(TAG, "$app requests ModuleProvider")
when (method) { return when (method) {
"getBinder" -> { "getBinder" -> Bundle().apply {
loadAllModules()
return Bundle().apply {
putBinder("binder", ManagerService.asBinder()) putBinder("binder", ManagerService.asBinder())
} }
}
else -> throw IllegalArgumentException("Invalid method name") else -> throw IllegalArgumentException("Invalid method name")
} }
} }
private fun loadAllModules() {
val list = mutableListOf<Module>()
for (pkg in context!!.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)) {
val app = pkg.applicationInfo ?: continue
if (app.metaData != null && app.metaData.containsKey("xposedminversion")) {
Module().apply {
apkPath = app.publicSourceDir
packageName = app.packageName
file = ModuleLoader.loadModule(apkPath)
}.also { list.add(it) }
Log.d(TAG, "send module ${app.packageName}")
}
}
allModules = list
}
override fun query(p0: Uri, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?): Cursor? { override fun query(p0: Uri, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?): Cursor? {
return null return null
} }

View File

@ -5,6 +5,8 @@ import android.graphics.drawable.GradientDrawable
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -27,8 +29,11 @@ fun AppItem(
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
checked: Boolean? = null, checked: Boolean? = null,
rightIcon: (@Composable () -> Unit)? = null,
additionalContent: (@Composable () -> Unit)? = null, additionalContent: (@Composable () -> Unit)? = null,
) { ) {
if (checked != null && rightIcon != null)
throw IllegalArgumentException("`checked` and `rightIcon` should not be both set")
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -67,6 +72,9 @@ fun AppItem(
modifier = Modifier.padding(start = 20.dp) modifier = Modifier.padding(start = 20.dp)
) )
} }
if (rightIcon != null) {
rightIcon()
}
} }
} }
} }
@ -82,7 +90,8 @@ private fun AppItemPreview() {
icon = shape, icon = shape,
label = "Sample App", label = "Sample App",
packageName = "org.lsposed.sample", packageName = "org.lsposed.sample",
onClick = {} onClick = {},
rightIcon = { Icon(Icons.Filled.ArrowForwardIos, null) }
) )
} }
} }

View File

@ -47,6 +47,7 @@ fun HomePage() {
ShizukuCard() ShizukuCard()
InfoCard() InfoCard()
SupportCard() SupportCard()
Spacer(Modifier)
} }
} }
} }
@ -84,11 +85,6 @@ private fun ShizukuCard() {
} }
ElevatedCard( ElevatedCard(
modifier = Modifier.clickable {
if (ShizukuApi.isBinderAvalable && !ShizukuApi.isPermissionGranted) {
Shizuku.requestPermission(114514)
}
},
colors = CardDefaults.elevatedCardColors(containerColor = run { colors = CardDefaults.elevatedCardColors(containerColor = run {
if (ShizukuApi.isPermissionGranted) MaterialTheme.colorScheme.secondaryContainer if (ShizukuApi.isPermissionGranted) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.errorContainer
@ -97,6 +93,11 @@ private fun ShizukuCard() {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable {
if (ShizukuApi.isBinderAvalable && !ShizukuApi.isPermissionGranted) {
Shizuku.requestPermission(114514)
}
}
.padding(24.dp), .padding(24.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {

View File

@ -13,8 +13,11 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
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.material.icons.filled.ArrowForwardIos
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -29,20 +32,24 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY 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.config.ConfigManager
import org.lsposed.lspatch.database.entity.Module
import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.component.AppItem
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.LocalSnackbarHost
import org.lsposed.lspatch.ui.util.observeState
import org.lsposed.lspatch.ui.util.setState
import org.lsposed.lspatch.ui.viewmodel.ManageViewModel import org.lsposed.lspatch.ui.viewmodel.ManageViewModel
import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
import java.io.IOException import java.io.IOException
private const val TAG = "ManagePage"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ManagePage() { fun ManagePage() {
@ -198,14 +205,8 @@ private fun Fab() {
@Composable @Composable
private fun Body() { private fun Body() {
val viewModel = viewModel<ManageViewModel>() val viewModel = viewModel<ManageViewModel>()
val navController = LocalNavController.current
LaunchedEffect(Unit) { val scope = rememberCoroutineScope()
if (LSPPackageManager.appList.isEmpty()) {
withContext(Dispatchers.IO) {
LSPPackageManager.fetchAppList()
}
}
}
if (viewModel.appList.isEmpty()) { if (viewModel.appList.isEmpty()) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
@ -220,18 +221,53 @@ private fun Body() {
) )
} }
} else { } else {
var scopeApp by rememberSaveable { mutableStateOf("") }
val isCancelled by navController.currentBackStackEntry!!.observeState<Boolean>("isCancelled")
LaunchedEffect(isCancelled) {
if (isCancelled == false) {
val selected = navController.currentBackStackEntry!!
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected").value!!.toSet()
Log.d(TAG, "Clear module list for $scopeApp")
ConfigManager.getModulesForApp(scopeApp).forEach {
ConfigManager.deactivateModule(scopeApp, it)
}
selected.forEach {
Log.d(TAG, "Activate ${it.app.packageName} for $scopeApp")
ConfigManager.activateModule(scopeApp, Module(it.app.packageName, it.app.sourceDir))
}
navController.currentBackStackEntry!!.setState("isCancelled", null)
}
}
LazyColumn { LazyColumn {
items( items(
items = viewModel.appList, items = viewModel.appList,
key = { it.first.app.packageName } key = { it.first.app.packageName }
) { ) {
var expanded by remember { mutableStateOf(false) }
Box {
AppItem( AppItem(
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)),
icon = LSPPackageManager.getIcon(it.first), icon = LSPPackageManager.getIcon(it.first),
label = it.first.label, label = it.first.label,
packageName = it.first.app.packageName, packageName = it.first.app.packageName,
onClick = {} onClick = {
) { scope.launch {
scopeApp = it.first.app.packageName
val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet()
navController.currentBackStackEntry!!.setState(
"selected",
SnapshotStateList<AppInfo>().apply {
LSPPackageManager.appList.filterTo(this) { activated.contains(it.app.packageName) }
}
)
navController.navigate(PageList.SelectApps.name + "?multiSelect=true")
}
},
onLongClick = {
// expanded = true
},
rightIcon = { if (it.second.useManager) Icon(Icons.Filled.ArrowForwardIos, null) },
additionalContent = {
val text = buildAnnotatedString { val text = buildAnnotatedString {
val (text, color) = val (text, color) =
if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary
@ -247,6 +283,11 @@ private fun Body() {
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
)
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
/* TODO */
}
} }
} }
} }

View File

@ -45,6 +45,7 @@ import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.util.* import org.lsposed.lspatch.ui.util.*
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.lspatch.util.ShizukuApi
@ -71,7 +72,7 @@ fun NewPatchPage(from: String, entry: NavBackStackEntry) {
runBlocking { runBlocking {
LSPPackageManager.getAppInfoFromApks(apks) LSPPackageManager.getAppInfoFromApks(apks)
.onSuccess { .onSuccess {
viewModel.configurePatch(it) viewModel.dispatch(ViewAction.ConfigurePatch(it))
} }
.onFailure { .onFailure {
lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: "Unknown error") } lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: "Unknown error") }
@ -86,7 +87,7 @@ fun NewPatchPage(from: String, entry: NavBackStackEntry) {
"storage" -> storageLauncher.launch(arrayOf("application/vnd.android.package-archive")) "storage" -> storageLauncher.launch(arrayOf("application/vnd.android.package-archive"))
"applist" -> { "applist" -> {
entry.savedStateHandle.getLiveData<AppInfo>("appInfo").observe(lifecycleOwner) { entry.savedStateHandle.getLiveData<AppInfo>("appInfo").observe(lifecycleOwner) {
viewModel.configurePatch(it) viewModel.dispatch(ViewAction.ConfigurePatch(it))
} }
navController.navigate(PageList.SelectApps.name + "?multiSelect=false") navController.navigate(PageList.SelectApps.name + "?multiSelect=false")
} }
@ -111,7 +112,7 @@ fun NewPatchPage(from: String, entry: NavBackStackEntry) {
) { innerPadding -> ) { innerPadding ->
if (viewModel.patchState == PatchState.CONFIGURING) { if (viewModel.patchState == PatchState.CONFIGURING) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
entry.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected", SnapshotStateList()).observe(lifecycleOwner) { entry.savedStateHandle.getLiveData("selected", SnapshotStateList<AppInfo>()).observe(lifecycleOwner) {
viewModel.embeddedModules = it viewModel.embeddedModules = it
} }
} }
@ -142,7 +143,7 @@ private fun ConfiguringFab() {
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 = { viewModel.submitPatch() } onClick = { viewModel.dispatch(ViewAction.SubmitPatch) }
) )
} }
@ -273,7 +274,7 @@ private fun DoPatchBody(modifier: Modifier) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (viewModel.logs.isEmpty()) { if (viewModel.logs.isEmpty()) {
viewModel.launchPatch() viewModel.dispatch(ViewAction.LaunchPatch)
} }
} }
@ -288,9 +289,7 @@ private fun DoPatchBody(modifier: Modifier) {
.animateContentSize(spring(stiffness = Spring.StiffnessLow)) .animateContentSize(spring(stiffness = Spring.StiffnessLow))
) { ) {
ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) { ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) {
CompositionLocalProvider( ProvideTextStyle(MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)) {
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
) {
val scrollState = rememberLazyListState() val scrollState = rememberLazyListState()
LazyColumn( LazyColumn(
state = scrollState, state = scrollState,

View File

@ -42,7 +42,7 @@ fun SelectAppsPage(multiSelect: Boolean) {
val filter: (AppInfo) -> Boolean = { val filter: (AppInfo) -> Boolean = {
val packageLowerCase = searchPackage.toLowerCase(Locale.current) val packageLowerCase = searchPackage.toLowerCase(Locale.current)
val contains = it.label.toLowerCase(Locale.current).contains(packageLowerCase) || it.app.packageName.contains(packageLowerCase) val contains = it.label.toLowerCase(Locale.current).contains(packageLowerCase) || it.app.packageName.contains(packageLowerCase)
if (multiSelect) contains && it.app.metaData?.get("xposedminversion") != null if (multiSelect) contains && it.isXposedModule
else contains && it.app.flags and ApplicationInfo.FLAG_SYSTEM == 0 else contains && it.app.flags and ApplicationInfo.FLAG_SYSTEM == 0
} }
@ -94,9 +94,13 @@ fun SelectAppsPage(multiSelect: Boolean) {
@Composable @Composable
private fun MultiSelectFab() { private fun MultiSelectFab() {
val navController = LocalNavController.current val navController = LocalNavController.current
FloatingActionButton(onClick = { navController.popBackStack() }) { FloatingActionButton(
Icon(Icons.Outlined.Done, stringResource(R.string.add)) onClick = {
} navController.previousBackStackEntry!!.setState("isCancelled", false)
navController.popBackStack()
},
content = { Icon(Icons.Outlined.Done, stringResource(R.string.add)) }
)
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)

View File

@ -77,7 +77,10 @@ private fun KeyStore() {
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.settings_keystore_custom)) }, text = { Text(stringResource(R.string.settings_keystore_custom)) },
onClick = { showDialog = true } onClick = {
dropDownExpanded = false
showDialog = true
}
) )
} }
} }
@ -160,7 +163,10 @@ private fun KeyStore() {
) )
}, },
text = { text = {
Column { Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
LaunchedEffect(interactionSource) { LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction -> interactionSource.interactions.collect { interaction ->
@ -178,9 +184,7 @@ private fun KeyStore() {
else -> null else -> null
} }
Text( Text(
modifier = Modifier modifier = Modifier.padding(bottom = 8.dp),
.align(Alignment.CenterHorizontally)
.padding(bottom = 8.dp),
text = wrongText ?: stringResource(R.string.settings_keystore_desc), text = wrongText ?: stringResource(R.string.settings_keystore_desc),
color = if (wrongText != null) MaterialTheme.colorScheme.error else Color.Unspecified color = if (wrongText != null) MaterialTheme.colorScheme.error else Color.Unspecified
) )

View File

@ -23,6 +23,12 @@ class NewPatchViewModel : ViewModel() {
SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
} }
sealed class ViewAction {
data class ConfigurePatch(val app: AppInfo) : ViewAction()
object SubmitPatch : ViewAction()
object LaunchPatch : ViewAction()
}
var patchState by mutableStateOf(PatchState.SELECTING) var patchState by mutableStateOf(PatchState.SELECTING)
private set private set
@ -31,10 +37,10 @@ class NewPatchViewModel : ViewModel() {
var overrideVersionCode by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false)
val sign = mutableStateListOf(false, true) val sign = mutableStateListOf(false, true)
var sigBypassLevel by mutableStateOf(2) var sigBypassLevel by mutableStateOf(2)
var embeddedModules = SnapshotStateList<AppInfo>()
lateinit var patchApp: AppInfo lateinit var patchApp: AppInfo
private set private set
lateinit var embeddedModules: SnapshotStateList<AppInfo>
lateinit var patchOptions: Patcher.Options lateinit var patchOptions: Patcher.Options
private set private set
@ -58,13 +64,21 @@ class NewPatchViewModel : ViewModel() {
} }
} }
fun configurePatch(app: AppInfo) { fun dispatch(action: ViewAction) {
when (action) {
is ViewAction.ConfigurePatch -> configurePatch(action.app)
is ViewAction.SubmitPatch -> submitPatch()
is ViewAction.LaunchPatch -> launchPatch()
}
}
private fun configurePatch(app: AppInfo) {
Log.d(TAG, "Configuring patch for ${app.app.packageName}") Log.d(TAG, "Configuring patch for ${app.app.packageName}")
patchApp = app patchApp = app
patchState = PatchState.CONFIGURING patchState = PatchState.CONFIGURING
} }
fun submitPatch() { private fun submitPatch() {
Log.d(TAG, "Submit patch") Log.d(TAG, "Submit patch")
if (useManager) embeddedModules.clear() if (useManager) embeddedModules.clear()
patchOptions = Patcher.Options( patchOptions = Patcher.Options(
@ -80,7 +94,7 @@ class NewPatchViewModel : ViewModel() {
patchState = PatchState.PATCHING patchState = PatchState.PATCHING
} }
fun launchPatch() { private fun launchPatch() {
logger.i("Launch patch") logger.i("Launch patch")
viewModelScope.launch { viewModelScope.launch {
patchState = try { patchState = try {

View File

@ -19,6 +19,7 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.lsposed.lspatch.Constants.PATCH_FILE_SUFFIX import org.lsposed.lspatch.Constants.PATCH_FILE_SUFFIX
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
import org.lsposed.lspatch.config.ConfigManager
import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.lspApp
import org.lsposed.patch.util.ManifestParser import org.lsposed.patch.util.ManifestParser
import java.io.File import java.io.File
@ -35,7 +36,10 @@ object LSPPackageManager {
private const val TAG = "LSPPackageManager" private const val TAG = "LSPPackageManager"
@Parcelize @Parcelize
class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable {
val isXposedModule: Boolean
get() = app.metaData?.get("xposedminversion") != null
}
const val STATUS_USER_CANCELLED = -2 const val STATUS_USER_CANCELLED = -2
@ -53,7 +57,11 @@ object LSPPackageManager {
collection.add(AppInfo(it, label.toString())) collection.add(AppInfo(it, label.toString()))
appIcon[it.packageName] = pm.getApplicationIcon(it) appIcon[it.packageName] = pm.getApplicationIcon(it)
} }
collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault())) { it.label }) collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
val modules = buildMap {
collection.forEach { if (it.isXposedModule) put(it.app.packageName, it.app.sourceDir) }
}
ConfigManager.updateModules(modules)
appList = collection appList = collection
} }
} }

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stub!"
android:textSize="50sp" />
</LinearLayout>

View File

@ -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("com.google.devtools.ksp") version "1.6.21-1.0.5"
id("dev.rikka.tools.refine") version "3.1.1" id("dev.rikka.tools.refine") version "3.1.1"
} }
} }