LSPatch Manager (Coding)

This commit is contained in:
Nullptr 2022-02-12 14:27:49 +08:00
parent 9129b42226
commit 6285ebe6c4
23 changed files with 739 additions and 73 deletions

View File

@ -24,8 +24,8 @@ import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.lsposed.lspatch.loader.util.FileUtils; import org.lsposed.lspatch.loader.util.FileUtils;
import org.lsposed.lspatch.loader.util.ModuleLoader;
import org.lsposed.lspatch.loader.util.XLog; import org.lsposed.lspatch.loader.util.XLog;
import org.lsposed.lspatch.manager.ModuleLoader;
import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.Constants;
import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.lspatch.share.PatchConfig;
import org.lsposed.lspd.config.ApplicationServiceClient; import org.lsposed.lspd.config.ApplicationServiceClient;

View File

@ -1,4 +1,4 @@
package org.lsposed.lspatch.loader.util; package org.lsposed.lspatch.manager;
import android.os.SharedMemory; import android.os.SharedMemory;
import android.system.ErrnoException; import android.system.ErrnoException;
@ -16,6 +16,7 @@ import java.util.List;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
public class ModuleLoader { public class ModuleLoader {
private static final String TAG = "LSPatch"; private static final String TAG = "LSPatch";
private static void readDexes(ZipFile apkFile, List<SharedMemory> preLoadedDexes) { private static void readDexes(ZipFile apkFile, List<SharedMemory> preLoadedDexes) {

View File

@ -9,6 +9,8 @@ val verName: String by rootProject.extra
val androidSourceCompatibility: JavaVersion by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra
val androidTargetCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra
val composeVersion = "1.2.0-alpha03"
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
@ -42,16 +44,31 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding = true compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = composeVersion
} }
} }
dependencies { dependencies {
implementation(project(":patch")) compileOnly(project(":patch"))
implementation(project(":imanager")) implementation(project(":imanager"))
implementation("androidx.core:core-ktx:1.7.0") implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.4.1") implementation("androidx.activity:activity-compose:1.5.0-alpha01")
implementation("androidx.compose.material:material-icons-extended:1.0.5")
implementation("androidx.compose.material3:material3:1.0.0-alpha04")
implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
implementation("androidx.compose.ui:ui:$composeVersion")
implementation("androidx.compose.ui:ui-tooling:$composeVersion")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01")
implementation("androidx.navigation:navigation-compose:2.5.0-alpha01")
implementation("androidx.preference:preference:1.2.0")
implementation("com.google.accompanist:accompanist-drawablepainter:0.24.2-alpha")
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.2-alpha")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
implementation("com.google.android.material:material:1.5.0") implementation("com.google.android.material:material:1.5.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.3") //implementation("dev.rikka.rikkax.material:material:2.1.0")
} }

View File

@ -13,10 +13,10 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.LSPatch"> android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.activity.MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -26,7 +26,7 @@
</activity> </activity>
<provider <provider
android:name=".ModuleProvider" android:name=".manager.ModuleProvider"
android:authorities="org.lsposed.lspatch.provider" android:authorities="org.lsposed.lspatch.provider"
android:enabled="true" android:enabled="true"
android:exported="true" /> android:exported="true" />

View File

@ -1,16 +1,13 @@
package org.lsposed.lspatch package org.lsposed.lspatch
import android.app.Application import android.app.Application
import android.content.Context
const val TAG = "LSPatch Manager"
class LSPApplication : Application() { class LSPApplication : Application() {
companion object {
const val TAG = "LSPatch Manager"
lateinit var appContext: Context
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
appContext = applicationContext
} }
} }

View File

@ -1,6 +1,5 @@
package org.lsposed.lspatch package org.lsposed.lspatch.manager
import org.lsposed.lspatch.manager.IManagerService
import org.lsposed.lspd.models.Module import org.lsposed.lspd.models.Module
class ManagerServiceImpl : IManagerService.Stub() { class ManagerServiceImpl : IManagerService.Stub() {

View File

@ -1,4 +1,4 @@
package org.lsposed.lspatch package org.lsposed.lspatch.manager
import android.content.ContentProvider import android.content.ContentProvider
import android.content.ContentValues import android.content.ContentValues
@ -8,11 +8,11 @@ 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.LSPApplication.Companion.TAG import org.lsposed.lspatch.TAG
import org.lsposed.lspd.models.Module import org.lsposed.lspd.models.Module
import org.lsposed.lspd.service.ConfigFileManager.loadModule
class ModuleProvider : ContentProvider() { class ModuleProvider : ContentProvider() {
companion object { companion object {
lateinit var allModules: List<Module> lateinit var allModules: List<Module>
} }
@ -43,7 +43,7 @@ class ModuleProvider : ContentProvider() {
Module().apply { Module().apply {
apkPath = app.publicSourceDir apkPath = app.publicSourceDir
packageName = app.packageName packageName = app.packageName
file = loadModule(apkPath) file = ModuleLoader.loadModule(apkPath)
}.also { list.add(it) } }.also { list.add(it) }
Log.d(TAG, "send module ${app.packageName}") Log.d(TAG, "send module ${app.packageName}")
} }

View File

@ -1,15 +0,0 @@
package org.lsposed.lspatch.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import org.lsposed.lspatch.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}

View File

@ -0,0 +1,97 @@
package org.lsposed.lspatch.ui.activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import org.lsposed.lspatch.ui.page.PageList
import org.lsposed.lspatch.ui.theme.LSPTheme
import org.lsposed.lspatch.ui.util.LocalNavController
import org.lsposed.lspatch.ui.util.currentRoute
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberAnimatedNavController()
val currentRoute = navController.currentRoute
val currentPage = if (currentRoute == null) null else PageList.valueOf(currentRoute.substringBefore('/'))
var mainPage by rememberSaveable { mutableStateOf(PageList.Home) }
LSPTheme {
CompositionLocalProvider(LocalNavController provides navController) {
Scaffold(
topBar = {
navController.currentBackStackEntry?.let {
CompositionLocalProvider(LocalViewModelStoreOwner provides it) {
currentPage?.topBar?.invoke()
}
}
},
bottomBar = {
MainNavigationBar(mainPage) {
mainPage = it
navController.navigate(it.name)
}
},
floatingActionButton = {
navController.currentBackStackEntry?.let {
CompositionLocalProvider(LocalViewModelStoreOwner provides it) {
currentPage?.fab?.invoke()
}
}
}
) { innerPadding ->
MainNavHost(navController, Modifier.padding(innerPadding))
}
}
}
}
}
}
@Composable
private fun MainNavHost(navController: NavHostController, modifier: Modifier) {
NavHost(
navController = navController,
startDestination = PageList.Home.name,
modifier = modifier
) {
PageList.values().forEach { page ->
val sb = StringBuilder(page.name)
page.arguments.forEach { sb.append("/{${it.name}}") }
composable(route = sb.toString(), arguments = page.arguments, content = page.body)
}
}
}
@Composable
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
NavigationBar(tonalElevation = 8.dp) {
arrayOf(PageList.Home, PageList.Patches, PageList.Repo, PageList.Settings).forEach {
NavigationBarItem(
selected = page == it,
onClick = { onClick(it) },
icon = {
if (page == it) Icon(it.iconSelected!!, it.title)
else Icon(it.iconNotSelected!!, it.title)
},
label = { Text(it.title) },
alwaysShowLabel = false
)
}
}
}

View File

@ -0,0 +1,73 @@
package org.lsposed.lspatch.ui.component
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.lsposed.lspatch.ui.theme.LSPTheme
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AppItem(
modifier: Modifier = Modifier,
icon: Drawable,
label: String,
packageName: String,
additionalInfo: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null
) {
Column(
modifier = modifier
.fillMaxWidth()
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
.padding(20.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = rememberDrawablePainter(icon),
contentDescription = label,
modifier = Modifier.size(32.dp),
tint = Color.Unspecified
)
Column(Modifier.padding(start = 20.dp)) {
Text(text = label, style = MaterialTheme.typography.bodyMedium)
Text(text = packageName, style = MaterialTheme.typography.bodySmall)
additionalInfo?.invoke()
}
}
}
}
@Preview
@Composable
private fun AppItemPreview() {
LSPTheme {
val shape = GradientDrawable()
shape.shape = GradientDrawable.RECTANGLE
shape.setColor(MaterialTheme.colorScheme.primary.toArgb())
AppItem(
icon = shape,
label = "Sample App",
packageName = "org.lsposed.sample",
onClick = {}
)
}
}

View File

@ -0,0 +1,89 @@
package org.lsposed.lspatch.ui.component
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
object SelectionColumnScope {
@Composable
fun SelectionItem(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector,
title: String,
desc: String? = null,
extraContent: (@Composable ColumnScope.() -> Unit)? = null
) {
Row(
modifier = modifier
.fillMaxWidth()
.heightIn(min = 64.dp)
.clip(RoundedCornerShape(4.dp))
.background(
animateColorAsState(
if (selected) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.inverseOnSurface
).value
)
.clickable { onClick() }
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null
)
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
if (desc != null || extraContent != null) {
AnimatedVisibility(
visible = selected,
enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),
exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom)
) {
Column {
desc?.let {
Text(
text = it,
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
}
extraContent?.invoke(this)
}
}
}
}
}
}
}
@Composable
fun SelectionColumn(
modifier: Modifier = Modifier,
content: @Composable() (SelectionColumnScope.() -> Unit)
) {
Column(
modifier = modifier.clip(RoundedCornerShape(32.dp)),
verticalArrangement = Arrangement.spacedBy(2.dp),
content = { SelectionColumnScope.content() }
)
}

View File

@ -0,0 +1,13 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.runtime.Composable
@Composable
fun HomePage() {
}
@Composable
fun HomeTopBar() {
}

View File

@ -0,0 +1,92 @@
package org.lsposed.lspatch.ui.page
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Api
import androidx.compose.material.icons.outlined.AutoFixHigh
import androidx.compose.material.icons.outlined.WorkOutline
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.lsposed.lspatch.R
import org.lsposed.lspatch.TAG
import org.lsposed.lspatch.ui.component.SelectionColumn
import org.lsposed.lspatch.ui.util.LocalNavController
import org.lsposed.lspatch.ui.viewmodel.AppInfo
@Composable
fun NewPatchFab() {
val navController = LocalNavController.current
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
.getLiveData<AppInfo>("appInfo").observeAsState()
if (patchApp != null) {
ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch_start)) },
icon = { Icon(Icons.Outlined.AutoFixHigh, null) },
onClick = { /*TODO*/ }
)
}
}
@Composable
fun NewPatchPage() {
val navController = LocalNavController.current
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
.getLiveData<AppInfo>("appInfo").observeAsState()
Log.d(TAG, "patchApp is $patchApp")
if (patchApp == null) {
navController.navigate(PageList.SelectApps.name + "/false")
} else {
var useManager by rememberSaveable { mutableStateOf(true) }
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
.padding(top = 24.dp)
) {
Text(text = patchApp!!.label, style = MaterialTheme.typography.headlineSmall)
Text(text = patchApp!!.app.packageName, style = MaterialTheme.typography.bodyLarge)
Text(
text = stringResource(R.string.patch_mode),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 24.dp, bottom = 12.dp)
)
SelectionColumn {
SelectionItem(
selected = useManager,
onClick = { useManager = true },
icon = Icons.Outlined.Api,
title = stringResource(R.string.patch_local),
desc = stringResource(R.string.patch_local_desc)
)
SelectionItem(
selected = !useManager,
onClick = { useManager = false },
icon = Icons.Outlined.WorkOutline,
title = stringResource(R.string.patch_portable),
desc = stringResource(R.string.patch_portable_desc),
extraContent = {
TextButton(
onClick = { /* TODO */ }
) {
Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge)
}
}
)
}
}
}
}

View File

@ -0,0 +1,76 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.GetApp
import androidx.compose.material.icons.filled.Healing
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.Healing
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.navArgument
import org.lsposed.lspatch.R
enum class PageList(
val iconSelected: ImageVector? = null,
val iconNotSelected: ImageVector? = null,
val arguments: List<NamedNavArgument> = emptyList(),
val topBar: @Composable () -> Unit,
val fab: @Composable () -> Unit = {},
val body: @Composable NavBackStackEntry.() -> Unit
) {
Home(
iconSelected = Icons.Filled.Home,
iconNotSelected = Icons.Outlined.Home,
topBar = { HomeTopBar() },
body = { HomePage() }
),
Patches(
iconSelected = Icons.Filled.Healing,
iconNotSelected = Icons.Outlined.Healing,
topBar = { PatchesTopBar() },
fab = { PatchesFab() },
body = {}
),
Repo(
iconSelected = Icons.Filled.GetApp,
iconNotSelected = Icons.Outlined.GetApp,
topBar = {},
body = {}
),
Settings(
iconSelected = Icons.Filled.Settings,
iconNotSelected = Icons.Outlined.Settings,
topBar = {},
body = {}
),
NewPatch(
topBar = {},
fab = { NewPatchFab() },
body = { NewPatchPage() }
),
SelectApps(
arguments = listOf(
navArgument("multiSelect") { type = NavType.BoolType }
),
topBar = {},
body = { SelectAppsPage(this) }
);
val title: String
@Composable get() = when (this) {
Home -> stringResource(R.string.app_name)
Patches -> stringResource(R.string.page_patches)
Repo -> stringResource(R.string.page_repo)
Settings -> stringResource(R.string.page_settings)
NewPatch -> stringResource(R.string.page_new_patch)
SelectApps -> stringResource(R.string.page_select_app)
}
}

View File

@ -0,0 +1,32 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.util.LocalNavController
@Composable
fun PatchesTopBar() {
SmallTopAppBar(
title = { Text(PageList.Patches.title) }
)
}
@Composable
fun PatchesFab() {
val navController = LocalNavController.current
FloatingActionButton(onClick = { navController.navigate(PageList.NewPatch.name) }) {
Icon(Icons.Filled.Add, stringResource(R.string.patches_add))
}
}
@Composable
fun PatchesPage() {
}

View File

@ -0,0 +1,64 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import org.lsposed.lspatch.ui.component.AppItem
import org.lsposed.lspatch.ui.util.LocalNavController
import org.lsposed.lspatch.ui.viewmodel.AppInfo
import org.lsposed.lspatch.ui.viewmodel.SelectAppViewModel
@Composable
fun SelectAppsPage(entry: NavBackStackEntry) {
val multiSelect = entry.arguments?.get("multiSelect") as? Boolean
?: throw IllegalArgumentException("multiSelect is null")
if (multiSelect) {
TODO("MultiSelect")
} else {
SelectSingle()
}
}
@Composable
private fun SelectSingle(
filter: (AppInfo) -> Boolean = { true }
) {
val context = LocalContext.current
val navController = LocalNavController.current
val viewModel = viewModel<SelectAppViewModel>()
val isRefreshing by viewModel.isRefreshing.collectAsState()
if (SelectAppViewModel.appList.isEmpty())
LaunchedEffect(viewModel) {
viewModel.loadAppList(context)
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.loadAppList(context) },
modifier = Modifier.fillMaxSize()
) {
LazyColumn {
items(SelectAppViewModel.appList) {
AppItem(
icon = it.icon,
label = it.label,
packageName = it.app.packageName,
onClick = {
navController.previousBackStackEntry!!.savedStateHandle.getLiveData<AppInfo>("appInfo").value = it
navController.popBackStack()
}
)
}
}
}
}

View File

@ -0,0 +1,41 @@
package org.lsposed.lspatch.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
@Composable
fun LSPTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
enableDynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
enableDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (isDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
isDarkTheme -> darkColorScheme()
else -> lightColorScheme()
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.background.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = !isDarkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,17 @@
package org.lsposed.lspatch.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)

View File

@ -0,0 +1,31 @@
package org.lsposed.lspatch.ui.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
val LocalNavController = compositionLocalOf<NavHostController> {
error("CompositionLocal LocalNavController not present")
}
val NavController.currentRoute: String?
@Composable get() = currentBackStackEntryAsState().value?.destination?.route
val NavController.startRoute: String?
get() = graph.findStartDestination().route
@Composable
fun NavController.isAtStartRoute(): Boolean = currentRoute == startRoute
fun NavController.navigateWithState(route: String?) {
navigate(route.toString()) {
popUpTo(startRoute.toString()) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}

View File

@ -0,0 +1,56 @@
package org.lsposed.lspatch.ui.viewmodel
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.lsposed.lspatch.TAG
class AppInfo(val app: ApplicationInfo, val icon: Drawable, val label: String)
class SelectAppViewModel : ViewModel() {
init {
Log.d(TAG, "SelectAppViewModel ${toString().substringAfterLast('@')} construct")
}
companion object {
var appList by mutableStateOf(listOf<AppInfo>())
private set
}
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
fun loadAppList(context: Context) {
viewModelScope.launch {
Log.d(TAG, "Start refresh apps")
_isRefreshing.emit(true)
val pm = context.packageManager
val collection = mutableListOf<AppInfo>()
withContext(Dispatchers.IO) {
pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach {
val icon = pm.getApplicationIcon(it)
val label = pm.getApplicationLabel(it)
collection.add(AppInfo(it, icon, label.toString()))
}
}
appList = collection
_isRefreshing.emit(false)
Log.d(TAG, "Refreshed ${appList.size} apps")
}
}
}

View File

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.LSPatch" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,3 +1,21 @@
<resources> <resources>
<string name="app_name">LSPatch</string> <string name="app_name">LSPatch</string>
<string name="page_repo">Repo</string>
<string name="page_settings">Settings</string>
<string name="page_select_app">Select App</string>
<!-- Patches Page -->
<string name="page_patches">Patches</string>
<string name="patches_add">Add</string>
<!-- New Patch Page -->
<string name="page_new_patch">New Patch</string>
<string name="patch_mode">Patch Mode</string>
<string name="patch_local">Local</string>
<string name="patch_local_desc">Patch an app without modules embedded.\nThe patched app need the manager running in background, and Xposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device.</string>
<string name="patch_portable">Portable</string>
<string name="patch_portable_desc">Patch an app with modules embedded.\nThe patched app can run without the manager, but cannot be managed dynamically.\nPortable patched apps can be used on devices that do not have LSPatch Manager installed.</string>
<string name="patch_embed_modules">Embed modules</string>
<string name="patch_start">Start Patch</string>
</resources> </resources>

View File

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.LSPatch" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>