diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 1632d1d..8aa59f4 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -4,7 +4,7 @@ import org.lsposed.patch.LSPatch object Patcher { class Options( - private val apkPaths: List, + private val apkPaths: Array, private val outputPath: String, private val debuggable: Boolean, private val sigbypassLevel: Int, @@ -18,7 +18,7 @@ object Patcher { ) { fun toStringArray(): Array { return arrayListOf().run { - add("-f"); + add("-f") addAll(apkPaths) add("-o"); add(outputPath) if (debuggable) add("-d") @@ -41,4 +41,4 @@ object Patcher { fun patch(options: Options) { LSPatch(*options.toStringArray()).doCommandLine() } -} \ No newline at end of file +} 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 index 232742a..9c793be 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt @@ -19,9 +19,9 @@ object SelectionColumnScope { @Composable fun SelectionItem( + modifier: Modifier = Modifier, selected: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier, icon: ImageVector, title: String, desc: String? = null, @@ -45,7 +45,8 @@ object SelectionColumnScope { ) { Icon( imageVector = icon, - contentDescription = null + contentDescription = null, + modifier = Modifier.size(24.dp) ) Column { Text( @@ -59,9 +60,9 @@ object SelectionColumnScope { exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom) ) { Column { - desc?.let { + if (desc != null) { Text( - text = it, + text = desc, modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.bodyMedium ) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt new file mode 100644 index 0000000..e0d828b --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt @@ -0,0 +1,51 @@ +package org.lsposed.lspatch.ui.component.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Api +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsCheckBox( + modifier: Modifier = Modifier, + checked: Boolean, + enabled: Boolean = true, + onClick: () -> Unit, + icon: ImageVector? = null, + title: String, + desc: String? = null, + extraContent: (@Composable ColumnScope.() -> Unit)? = null +) { + SettingsSlot(modifier, enabled, onClick, icon, title, desc, extraContent) { + Checkbox(checked = checked, onCheckedChange = { onClick() }) + } +} + +@Preview +@Composable +private fun SettingsCheckBoxPreview() { + var checked1 by remember { mutableStateOf(false) } + var checked2 by remember { mutableStateOf(false) } + Column { + SettingsCheckBox( + checked = checked1, + onClick = { checked1 = !checked1 }, + title = "Title", + desc = "Description" + ) + SettingsCheckBox( + checked = checked2, + onClick = { checked2 = !checked2 }, + icon = Icons.Outlined.Api, + title = "Title", + desc = "Description" + ) + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt new file mode 100644 index 0000000..7b4fea1 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt @@ -0,0 +1,76 @@ +package org.lsposed.lspatch.ui.component.settings + +import androidx.compose.foundation.clickable +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.draw.alpha +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsSlot( + modifier: Modifier, + enabled: Boolean, + onClick: () -> Unit, + icon: ImageVector? = null, + title: String, + desc: String?, + extraContent: (@Composable ColumnScope.() -> Unit)? = null, + action: (@Composable RowScope.() -> Unit)?, +) { + Row( + modifier = modifier + .fillMaxWidth() then ( + if (enabled) Modifier + .alpha(1f) + .clickable(onClick = onClick) + else Modifier.alpha(0.5f) + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = modifier.size(24.dp), + contentAlignment = Alignment.Center, + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + } + Column(Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.titleMedium) + Column { + if (desc != null) { + Text( + text = desc, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.alpha(0.75f).padding(top = 4.dp) + ) + } + extraContent?.invoke(this) + } + } + action?.invoke(this) + } +} + +@Composable +fun SettingsItem( + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit, + icon: ImageVector? = null, + title: String, + desc: String? = null, + extraContent: (@Composable ColumnScope.() -> Unit)? = null +) = SettingsSlot(modifier, enabled, onClick, icon, title, desc, extraContent, null) 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 index 25d9f2e..a3f820d 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchPage.kt @@ -2,12 +2,15 @@ package org.lsposed.lspatch.ui.page import android.util.Log import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height 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.BugReport import androidx.compose.material.icons.outlined.WorkOutline import androidx.compose.material3.* import androidx.compose.runtime.* @@ -17,76 +20,158 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.R import org.lsposed.lspatch.TAG import org.lsposed.lspatch.ui.component.SelectionColumn +import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox +import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.util.LocalNavController import org.lsposed.lspatch.ui.viewmodel.AppInfo +class NewPatchPageViewModel : ViewModel() { + var patchApp by mutableStateOf(null) + var confirm by mutableStateOf(false) + var patchOptions by mutableStateOf(null) +} + @Composable fun NewPatchFab() { - val navController = LocalNavController.current - val patchApp by navController.currentBackStackEntry!!.savedStateHandle - .getLiveData("appInfo").observeAsState() - if (patchApp != null) { + val viewModel = viewModel() + if (viewModel.patchApp != null) { ExtendedFloatingActionButton( text = { Text(stringResource(R.string.patch_start)) }, icon = { Icon(Icons.Outlined.AutoFixHigh, null) }, - onClick = { /*TODO*/ } + onClick = { viewModel.confirm = true } ) } } @Composable fun NewPatchPage() { + val viewModel = viewModel() val navController = LocalNavController.current - val patchApp by navController.currentBackStackEntry!!.savedStateHandle + val appInfo by navController.currentBackStackEntry!!.savedStateHandle .getLiveData("appInfo").observeAsState() + viewModel.patchApp = appInfo - Log.d(TAG, "patchApp is $patchApp") - if (patchApp == null) { - navController.navigate(PageList.SelectApps.name + "/false") - } else { - var useManager by rememberSaveable { mutableStateOf(true) } + Log.d(TAG, "confirm = ${viewModel.confirm}") - 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) - } - } - ) - } - } + when { + viewModel.patchApp == null -> navController.navigate(PageList.SelectApps.name + "/false") + viewModel.patchOptions == null -> PatchOptionsPage(viewModel.patchApp!!, viewModel.confirm) + else -> PatchingPage(viewModel.patchOptions!!) } } + +@Composable +private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) { + val viewModel = viewModel() + var useManager by rememberSaveable { mutableStateOf(true) } + var debuggable by rememberSaveable { mutableStateOf(false) } + var v1 by rememberSaveable { mutableStateOf(false) } + var v2 by rememberSaveable { mutableStateOf(true) } + var v3 by rememberSaveable { mutableStateOf(true) } + val sigBypassLevel by rememberSaveable { mutableStateOf(2) } + var overrideVersionCode by rememberSaveable { mutableStateOf(false) } + + if (confirm) LaunchedEffect(patchApp) { + viewModel.patchOptions = Patcher.Options( + apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk + debuggable = debuggable, + sigbypassLevel = sigBypassLevel, + v1 = v1, v2 = v2, v3 = v3, + useManager = useManager, + overrideVersionCode = overrideVersionCode, + verbose = true, + embeddedModules = emptyList() // TODO: Embed modules + ) + } + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(top = 24.dp) + ) { + Text( + text = patchApp.label, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Text( + text = patchApp.app.packageName, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Text( + text = stringResource(R.string.patch_mode), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 24.dp, bottom = 12.dp) + ) + SelectionColumn(Modifier.padding(horizontal = 24.dp)) { + 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) + } + } + ) + } + SettingsCheckBox( + checked = debuggable, + onClick = { debuggable = !debuggable }, + icon = Icons.Outlined.BugReport, + title = stringResource(R.string.patch_debuggable) + ) + SettingsCheckBox( + checked = v1, + onClick = { v1 = !v1 }, + title = stringResource(R.string.patch_v1) + ) + SettingsCheckBox( + checked = v2, + onClick = { v2 = !v2 }, + title = stringResource(R.string.patch_v2) + ) + SettingsCheckBox( + checked = v3, + onClick = { v3 = !v3 }, + title = stringResource(R.string.patch_v3) + ) + SettingsItem( + onClick = { /*TODO*/ }, + title = stringResource(R.string.patch_sigbypasslv), + desc = stringResource(R.string.patch_sigbypasslv_desc) + ) + SettingsCheckBox( + checked = overrideVersionCode, + onClick = { overrideVersionCode = !overrideVersionCode }, + title = stringResource(R.string.patch_override_version_code), + desc = stringResource(R.string.patch_override_version_code_desc) + ) + Spacer(Modifier.height(56.dp)) + } +} + +@Composable +private fun PatchingPage(patcherOptions: Patcher.Options) { + +} diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 1c29720..8570b26 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -17,5 +17,13 @@ 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 + Debuggable + V1 signature + V2 signature + V3 signature + Signature bypass level + lv0: off\nlv1: bypass pm\nlv2: bypass openat (libc) + Override version code + Override the patched app\'s version code to 1\nThis allows downgrade installation Start Patch