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);
+}