diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index 0da1d82..bac011f 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> + + Unit +) { + val transition = rememberInfiniteTransition() + val translateAnim by transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + tween(durationMillis = 1200, easing = FastOutSlowInEasing), + RepeatMode.Reverse + ) + ) + + val brush = Brush.linearGradient( + colors = if (enabled) ShimmerColorShades else List(3) { ShimmerColorShades[0] }, + start = Offset(10f, 10f), + end = Offset(translateAnim, translateAnim) + ) + + Surface(modifier.background(brush)) { + content(ShimmerScope(brush)) + } +} + +@Preview +@Composable +private fun ShimmerPreview() { + ShimmerAnimation { + Column(modifier = Modifier.padding(16.dp)) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .size(250.dp) + .background(brush = brush) + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(30.dp) + .padding(vertical = 8.dp) + .background(brush = brush) + ) + } + } +} 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 a3f820d..b18c4ef 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 @@ -1,11 +1,17 @@ package org.lsposed.lspatch.ui.page +import android.os.Environment 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.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Api @@ -18,7 +24,9 @@ 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.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel @@ -26,25 +34,32 @@ 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.ShimmerAnimation 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.util.isScrolledToEnd +import org.lsposed.lspatch.ui.util.lastItemIndex import org.lsposed.lspatch.ui.viewmodel.AppInfo +import org.lsposed.patch.util.Logger + +enum class PatchState { + SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED +} class NewPatchPageViewModel : ViewModel() { - var patchApp by mutableStateOf(null) - var confirm by mutableStateOf(false) + var patchState by mutableStateOf(PatchState.SELECTING) var patchOptions by mutableStateOf(null) } @Composable fun NewPatchFab() { val viewModel = viewModel() - if (viewModel.patchApp != null) { + if (viewModel.patchState == PatchState.CONFIGURING) { ExtendedFloatingActionButton( text = { Text(stringResource(R.string.patch_start)) }, icon = { Icon(Icons.Outlined.AutoFixHigh, null) }, - onClick = { viewModel.confirm = true } + onClick = { viewModel.patchState = PatchState.SUBMITTING } ) } } @@ -53,21 +68,20 @@ fun NewPatchFab() { fun NewPatchPage() { val viewModel = viewModel() val navController = LocalNavController.current - val appInfo by navController.currentBackStackEntry!!.savedStateHandle + val patchApp by navController.currentBackStackEntry!!.savedStateHandle .getLiveData("appInfo").observeAsState() - viewModel.patchApp = appInfo + if (viewModel.patchState == PatchState.SELECTING && patchApp != null) viewModel.patchState = PatchState.CONFIGURING - Log.d(TAG, "confirm = ${viewModel.confirm}") - - when { - viewModel.patchApp == null -> navController.navigate(PageList.SelectApps.name + "/false") - viewModel.patchOptions == null -> PatchOptionsPage(viewModel.patchApp!!, viewModel.confirm) - else -> PatchingPage(viewModel.patchOptions!!) + Log.d(TAG, "NewPatchPage: ${viewModel.patchState}") + when (viewModel.patchState) { + PatchState.SELECTING -> navController.navigate(PageList.SelectApps.name + "/false") + PatchState.CONFIGURING, PatchState.SUBMITTING -> PatchOptionsPage(patchApp!!) + PatchState.PATCHING, PatchState.FINISHED -> DoPatchPage(viewModel.patchOptions!!) } } @Composable -private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) { +private fun PatchOptionsPage(patchApp: AppInfo) { val viewModel = viewModel() var useManager by rememberSaveable { mutableStateOf(true) } var debuggable by rememberSaveable { mutableStateOf(false) } @@ -77,9 +91,11 @@ private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) { val sigBypassLevel by rememberSaveable { mutableStateOf(2) } var overrideVersionCode by rememberSaveable { mutableStateOf(false) } - if (confirm) LaunchedEffect(patchApp) { + if (viewModel.patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) { + val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path viewModel.patchOptions = Patcher.Options( apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk + outputPath = downloadDir, debuggable = debuggable, sigbypassLevel = sigBypassLevel, v1 = v1, v2 = v2, v3 = v3, @@ -88,6 +104,7 @@ private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) { verbose = true, embeddedModules = emptyList() // TODO: Embed modules ) + viewModel.patchState = PatchState.PATCHING } Column( @@ -172,6 +189,89 @@ private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) { } @Composable -private fun PatchingPage(patcherOptions: Patcher.Options) { +private fun DoPatchPage(patcherOptions: Patcher.Options) { + val viewModel = viewModel() + val navController = LocalNavController.current + val logs = remember { mutableStateListOf>() } + val logger = remember { + object : Logger() { + override fun d(msg: String) { + if (verbose) { + Log.d(TAG, msg) + logs += Log.DEBUG to msg + } + } + override fun i(msg: String) { + Log.i(TAG, msg) + logs += Log.INFO to msg + } + + override fun e(msg: String) { + Log.e(TAG, msg) + logs += Log.ERROR to msg + } + } + } + + LaunchedEffect(patcherOptions) { + Patcher.patch(logger, patcherOptions) + viewModel.patchState = PatchState.FINISHED + } + + Column( + Modifier + .fillMaxSize() + .padding(24.dp) + .wrapContentHeight() + .animateContentSize(spring(stiffness = Spring.StiffnessLow)) + ) { + val patching by remember { derivedStateOf { viewModel.patchState == PatchState.PATCHING } } + ShimmerAnimation(enabled = patching) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + ) { + val scrollState = rememberLazyListState() + LazyColumn( + state = scrollState, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 300.dp) + .clip(RoundedCornerShape(32.dp)) + .background(brush) + .padding(horizontal = 24.dp, vertical = 18.dp) + ) { + items(logs) { + when (it.first) { + Log.DEBUG -> Text(text = it.second) + Log.INFO -> Text(text = it.second) + Log.ERROR -> Text(text = it.second, color = MaterialTheme.colorScheme.error) + } + } + } + + LaunchedEffect(scrollState.lastItemIndex) { + if (!scrollState.isScrolledToEnd) { + scrollState.animateScrollToItem(scrollState.lastItemIndex!!) + } + } + } + } + + if (!patching) { + Row(Modifier.padding(top = 12.dp)) { + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier.weight(1f), + content = { Text(stringResource(R.string.patch_return)) } + ) + Spacer(Modifier.weight(0.2f)) + Button( + onClick = { /* TODO: Install */ }, + modifier = Modifier.weight(1f), + content = { Text(stringResource(R.string.patch_install)) } + ) + } + } + } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt new file mode 100644 index 0000000..0a90329 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt @@ -0,0 +1,12 @@ +package org.lsposed.lspatch.ui.util + +import androidx.compose.foundation.lazy.LazyListState + +val LazyListState.lastVisibleItemIndex + get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index + +val LazyListState.lastItemIndex + get() = layoutInfo.totalItemsCount.let { if (it == 0) null else it } + +val LazyListState.isScrolledToEnd + get() = lastVisibleItemIndex == lastItemIndex diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 8570b26..9c4e659 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -26,4 +26,6 @@ Override version code Override the patched app\'s version code to 1\nThis allows downgrade installation Start Patch + Return + Install diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index ede2343..611820f 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -23,6 +23,8 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.patch.util.ApkSignatureHelper; +import org.lsposed.patch.util.JavaLogger; +import org.lsposed.patch.util.Logger; import org.lsposed.patch.util.ManifestParser; import java.io.ByteArrayInputStream; @@ -114,15 +116,19 @@ public class LSPatch { private final JCommander jCommander; - public LSPatch(String... args) { + private final Logger logger; + + public LSPatch(Logger logger, String... args) { jCommander = JCommander.newBuilder() .addObject(this) .build(); jCommander.parse(args); + this.logger = logger; + logger.verbose = verbose; } public static void main(String... args) throws IOException { - LSPatch lsPatch = new LSPatch(args); + LSPatch lsPatch = new LSPatch(new JavaLogger(), args); try { lsPatch.doCommandLine(); } catch (PatchError e) { @@ -157,7 +163,7 @@ public class LSPatch { if (outputFile.exists() && !forceOverwrite) throw new PatchError(outputPath + " exists. Use --force to overwrite"); - System.out.println("Processing " + srcApkFile + " -> " + outputFile); + logger.i("Processing " + srcApkFile + " -> " + outputFile); patch(srcApkFile, outputFile); } @@ -170,16 +176,15 @@ public class LSPatch { File tmpApk = Files.createTempFile(srcApkFile.getName(), "unsigned").toFile(); tmpApk.delete(); - if (verbose) - System.out.println("apk path: " + srcApkFile); + logger.d("apk path: " + srcApkFile); - System.out.println("Parsing original apk..."); + logger.i("Parsing original apk..."); try (var dstZFile = ZFile.openReadWrite(tmpApk, Z_FILE_OPTIONS); var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) { // sign apk - System.out.println("Register apk signer..."); + logger.i("Register apk signer..."); try { var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) { @@ -205,8 +210,7 @@ public class LSPatch { throw new PatchError("get original signature failed"); } - if (verbose) - System.out.println("Original signature\n" + originalSignature); + logger.d("Original signature\n" + originalSignature); } // copy out manifest file from zlib @@ -222,11 +226,10 @@ public class LSPatch { throw new PatchError("Failed to parse AndroidManifest.xml"); appComponentFactory = pair.appComponentFactory == null ? "" : pair.appComponentFactory; - if (verbose) - System.out.println("original appComponentFactory class: " + appComponentFactory); + logger.d("original appComponentFactory class: " + appComponentFactory); } - System.out.println("Patching apk..."); + logger.i("Patching apk..."); // modify manifest try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open()))) { dstZFile.add(ANDROID_MANIFEST_XML, is); @@ -234,8 +237,7 @@ public class LSPatch { throw new PatchError("Error when modifying manifest", e); } - if (verbose) - System.out.println("Adding native lib.."); + logger.d("Adding native lib.."); // copy so and dex files into the unzipped apk // do not put liblspd.so into apk!lib because x86 native bridge causes crash @@ -247,12 +249,10 @@ public class LSPatch { // More exception info throw new PatchError("Error when adding native lib", e); } - if (verbose) - System.out.println("added " + entryName); + logger.d("added " + entryName); } - if (verbose) - System.out.println("Adding dex.."); + logger.d("Adding dex.."); try (var is = getClass().getClassLoader().getResourceAsStream("assets/dex/loader.dex")) { dstZFile.add("classes.dex", is); @@ -278,8 +278,7 @@ public class LSPatch { Set apkArchs = new HashSet<>(); - if (verbose) - System.out.println("Search target apk library arch..."); + logger.d("Search target apk library arch..."); for (StoredEntry storedEntry : srcZFile.entries()) { var name = storedEntry.getCentralDirectoryHeader().getName(); @@ -291,7 +290,7 @@ public class LSPatch { if (apkArchs.isEmpty()) apkArchs.addAll(ARCHES); apkArchs.removeIf((arch) -> { if (!ARCHES.contains(arch) && !arch.equals("armeabi")) { - System.err.println("Warning: unsupported arch " + arch + ". Skipping..."); + logger.e("Warning: unsupported arch " + arch + ". Skipping..."); return true; } return false; @@ -300,8 +299,7 @@ public class LSPatch { embedModules(dstZFile); // create zip link - if (verbose) - System.out.println("Creating nested apk link..."); + logger.d("Creating nested apk link..."); for (StoredEntry entry : srcZFile.entries()) { String name = entry.getCentralDirectoryHeader().getName(); @@ -314,12 +312,12 @@ public class LSPatch { dstZFile.realign(); - System.out.println("Writing apk..."); + logger.i("Writing apk..."); } finally { try { outputFile.delete(); FileUtils.moveFile(tmpApk, outputFile); - System.out.println("Done. Output APK: " + outputFile.getAbsolutePath()); + logger.i("Done. Output APK: " + outputFile.getAbsolutePath()); } catch (Throwable e) { throw new PatchError("Error writing apk", e); } @@ -327,7 +325,7 @@ public class LSPatch { } private void embedModules(ZFile zFile) { - System.out.println("Embedding modules..."); + logger.i("Embedding modules..."); for (var module : modules) { File file = new File(module); try (var apk = ZFile.openReadOnly(new File(module)); @@ -336,10 +334,10 @@ public class LSPatch { ) { var manifest = Objects.requireNonNull(ManifestParser.parseManifestFile(xmlIs)); var packageName = manifest.packageName; - System.out.println(" - " + packageName); + logger.i(" - " + packageName); zFile.add("assets/lspatch/modules/" + packageName + ".bin", fileIs); } catch (NullPointerException | IOException e) { - System.err.println(module + " does not exist or is not a valid apk file."); + logger.e(module + " does not exist or is not a valid apk file."); } } } diff --git a/patch/src/main/java/org/lsposed/patch/util/JavaLogger.java b/patch/src/main/java/org/lsposed/patch/util/JavaLogger.java new file mode 100644 index 0000000..c48d543 --- /dev/null +++ b/patch/src/main/java/org/lsposed/patch/util/JavaLogger.java @@ -0,0 +1,19 @@ +package org.lsposed.patch.util; + +public class JavaLogger extends Logger { + + @Override + public void d(String msg) { + if (verbose) System.out.println(msg); + } + + @Override + public void i(String msg) { + System.out.println(msg); + } + + @Override + public void e(String msg) { + System.err.println(msg); + } +} diff --git a/patch/src/main/java/org/lsposed/patch/util/Logger.java b/patch/src/main/java/org/lsposed/patch/util/Logger.java new file mode 100644 index 0000000..59aa0b7 --- /dev/null +++ b/patch/src/main/java/org/lsposed/patch/util/Logger.java @@ -0,0 +1,12 @@ +package org.lsposed.patch.util; + +public abstract class Logger { + + public boolean verbose = false; + + abstract public void d(String msg); + + abstract public void i(String msg); + + abstract public void e(String msg); +}