From 6285ebe6c4c4b9ea587e8d1f18d609f390d5950a Mon Sep 17 00:00:00 2001 From: Nullptr <52071314+Dr-TSNG@users.noreply.github.com> Date: Sat, 12 Feb 2022 14:27:49 +0800 Subject: [PATCH] LSPatch Manager (Coding) --- .../lspatch/loader/LSPApplication.java | 2 +- .../lspatch/manager}/ModuleLoader.java | 3 +- manager/build.gradle.kts | 25 ++++- manager/src/main/AndroidManifest.xml | 8 +- .../org/lsposed/lspatch/LSPApplication.kt | 11 +-- .../{ => manager}/ManagerServiceImpl.kt | 5 +- .../lspatch/{ => manager}/ModuleProvider.kt | 10 +- .../org/lsposed/lspatch/ui/MainActivity.kt | 15 --- .../lspatch/ui/activity/MainActivity.kt | 97 +++++++++++++++++++ .../lsposed/lspatch/ui/component/AppItem.kt | 73 ++++++++++++++ .../lspatch/ui/component/SelectionColumn.kt | 89 +++++++++++++++++ .../org/lsposed/lspatch/ui/page/HomePage.kt | 13 +++ .../lsposed/lspatch/ui/page/NewPatchPage.kt | 92 ++++++++++++++++++ .../org/lsposed/lspatch/ui/page/PageList.kt | 76 +++++++++++++++ .../lsposed/lspatch/ui/page/PatchesPage.kt | 32 ++++++ .../lsposed/lspatch/ui/page/SelectAppsPage.kt | 64 ++++++++++++ .../org/lsposed/lspatch/ui/theme/Theme.kt | 41 ++++++++ .../java/org/lsposed/lspatch/ui/theme/Type.kt | 17 ++++ .../org/lsposed/lspatch/ui/util/Navigation.kt | 31 ++++++ .../ui/viewmodel/SelectAppViewModel.kt | 56 +++++++++++ manager/src/main/res/values-night/themes.xml | 16 --- manager/src/main/res/values/strings.xml | 20 +++- manager/src/main/res/values/themes.xml | 16 --- 23 files changed, 739 insertions(+), 73 deletions(-) rename {app/src/main/java/org/lsposed/lspatch/loader/util => imanager/src/main/java/org/lsposed/lspatch/manager}/ModuleLoader.java (98%) rename manager/src/main/java/org/lsposed/lspatch/{ => manager}/ManagerServiceImpl.kt (69%) rename manager/src/main/java/org/lsposed/lspatch/{ => manager}/ModuleProvider.kt (91%) delete mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/MainActivity.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/page/PatchesPage.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/theme/Theme.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/theme/Type.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt create mode 100644 manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt delete mode 100644 manager/src/main/res/values-night/themes.xml delete mode 100644 manager/src/main/res/values/themes.xml diff --git a/app/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/app/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java index 4bd0f1d..ffd498b 100644 --- a/app/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java +++ b/app/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java @@ -24,8 +24,8 @@ import android.util.Log; import com.google.gson.Gson; 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.manager.ModuleLoader; import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.lspd.config.ApplicationServiceClient; diff --git a/app/src/main/java/org/lsposed/lspatch/loader/util/ModuleLoader.java b/imanager/src/main/java/org/lsposed/lspatch/manager/ModuleLoader.java similarity index 98% rename from app/src/main/java/org/lsposed/lspatch/loader/util/ModuleLoader.java rename to imanager/src/main/java/org/lsposed/lspatch/manager/ModuleLoader.java index d6a90bc..c2ee01c 100644 --- a/app/src/main/java/org/lsposed/lspatch/loader/util/ModuleLoader.java +++ b/imanager/src/main/java/org/lsposed/lspatch/manager/ModuleLoader.java @@ -1,4 +1,4 @@ -package org.lsposed.lspatch.loader.util; +package org.lsposed.lspatch.manager; import android.os.SharedMemory; import android.system.ErrnoException; @@ -16,6 +16,7 @@ import java.util.List; import java.util.zip.ZipFile; public class ModuleLoader { + private static final String TAG = "LSPatch"; private static void readDexes(ZipFile apkFile, List preLoadedDexes) { diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index ee0ef5b..23b3187 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -9,6 +9,8 @@ val verName: String by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra +val composeVersion = "1.2.0-alpha03" + plugins { id("com.android.application") kotlin("android") @@ -42,16 +44,31 @@ android { } buildFeatures { - viewBinding = true + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = composeVersion } } dependencies { - implementation(project(":patch")) + compileOnly(project(":patch")) implementation(project(":imanager")) 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("androidx.constraintlayout:constraintlayout:2.1.3") + //implementation("dev.rikka.rikkax.material:material:2.1.0") } diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index 9442ab6..0da1d82 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -13,10 +13,10 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.LSPatch"> + android:theme="@style/Theme.Material3.DayNight.NoActionBar"> @@ -26,10 +26,10 @@ - \ No newline at end of file + diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt index 0102924..5ef0139 100644 --- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt @@ -1,16 +1,13 @@ package org.lsposed.lspatch import android.app.Application -import android.content.Context + +const val TAG = "LSPatch Manager" class LSPApplication : Application() { - companion object { - const val TAG = "LSPatch Manager" - lateinit var appContext: Context - } override fun onCreate() { super.onCreate() - appContext = applicationContext + } -} \ No newline at end of file +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ManagerServiceImpl.kt b/manager/src/main/java/org/lsposed/lspatch/manager/ManagerServiceImpl.kt similarity index 69% rename from manager/src/main/java/org/lsposed/lspatch/ManagerServiceImpl.kt rename to manager/src/main/java/org/lsposed/lspatch/manager/ManagerServiceImpl.kt index 16ca1d6..df25a63 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ManagerServiceImpl.kt +++ b/manager/src/main/java/org/lsposed/lspatch/manager/ManagerServiceImpl.kt @@ -1,10 +1,9 @@ -package org.lsposed.lspatch +package org.lsposed.lspatch.manager -import org.lsposed.lspatch.manager.IManagerService import org.lsposed.lspd.models.Module class ManagerServiceImpl : IManagerService.Stub() { override fun getModules(): List { return ModuleProvider.allModules } -} \ No newline at end of file +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ModuleProvider.kt b/manager/src/main/java/org/lsposed/lspatch/manager/ModuleProvider.kt similarity index 91% rename from manager/src/main/java/org/lsposed/lspatch/ModuleProvider.kt rename to manager/src/main/java/org/lsposed/lspatch/manager/ModuleProvider.kt index c279ec3..e04f404 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ModuleProvider.kt +++ b/manager/src/main/java/org/lsposed/lspatch/manager/ModuleProvider.kt @@ -1,4 +1,4 @@ -package org.lsposed.lspatch +package org.lsposed.lspatch.manager import android.content.ContentProvider import android.content.ContentValues @@ -8,11 +8,11 @@ import android.net.Uri import android.os.Binder import android.os.Bundle 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.service.ConfigFileManager.loadModule class ModuleProvider : ContentProvider() { + companion object { lateinit var allModules: List } @@ -43,7 +43,7 @@ class ModuleProvider : ContentProvider() { Module().apply { apkPath = app.publicSourceDir packageName = app.packageName - file = loadModule(apkPath) + file = ModuleLoader.loadModule(apkPath) }.also { list.add(it) } Log.d(TAG, "send module ${app.packageName}") } @@ -70,4 +70,4 @@ class ModuleProvider : ContentProvider() { override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int { return 0 } -} \ No newline at end of file +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/MainActivity.kt b/manager/src/main/java/org/lsposed/lspatch/ui/MainActivity.kt deleted file mode 100644 index 69764df..0000000 --- a/manager/src/main/java/org/lsposed/lspatch/ui/MainActivity.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt new file mode 100644 index 0000000..de0978d --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt @@ -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 + ) + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt new file mode 100644 index 0000000..f3b5384 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt @@ -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 = {} + ) + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt new file mode 100644 index 0000000..232742a --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt @@ -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() } + ) +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt new file mode 100644 index 0000000..3f8e25e --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/HomePage.kt @@ -0,0 +1,13 @@ +package org.lsposed.lspatch.ui.page + +import androidx.compose.runtime.Composable + +@Composable +fun HomePage() { + +} + +@Composable +fun HomeTopBar() { + +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt new file mode 100644 index 0000000..25d9f2e --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt @@ -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").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").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) + } + } + ) + } + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt new file mode 100644 index 0000000..91f9fcd --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt @@ -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 = 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) + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchesPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchesPage.kt new file mode 100644 index 0000000..e7e43f4 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchesPage.kt @@ -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() { + +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt new file mode 100644 index 0000000..8857109 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsPage.kt @@ -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() + 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").value = it + navController.popBackStack() + } + ) + } + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/theme/Theme.kt b/manager/src/main/java/org/lsposed/lspatch/ui/theme/Theme.kt new file mode 100644 index 0000000..f2ec166 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/theme/Theme.kt @@ -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 + ) +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/theme/Type.kt b/manager/src/main/java/org/lsposed/lspatch/ui/theme/Type.kt new file mode 100644 index 0000000..6c982ac --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/theme/Type.kt @@ -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 + ) +) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt new file mode 100644 index 0000000..8b811ad --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Navigation.kt @@ -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 { + 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 + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt new file mode 100644 index 0000000..e14d787 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppViewModel.kt @@ -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()) + private set + } + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow + 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() + 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") + } + } +} diff --git a/manager/src/main/res/values-night/themes.xml b/manager/src/main/res/values-night/themes.xml deleted file mode 100644 index 7d77c56..0000000 --- a/manager/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 057f035..1c29720 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -1,3 +1,21 @@ LSPatch - \ No newline at end of file + + Repo + Settings + Select App + + + Patches + Add + + + New Patch + Patch Mode + Local + 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. + Portable + 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. + Embed modules + Start Patch + diff --git a/manager/src/main/res/values/themes.xml b/manager/src/main/res/values/themes.xml deleted file mode 100644 index 02a439c..0000000 --- a/manager/src/main/res/values/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file