Package module

This commit is contained in:
Nullptr 2022-02-13 19:30:58 +08:00
parent bb7bfaad26
commit 78420d8758
9 changed files with 274 additions and 49 deletions

View File

@ -7,6 +7,10 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"

View File

@ -1,6 +1,9 @@
package org.lsposed.lspatch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
object Patcher {
class Options(
@ -38,7 +41,9 @@ object Patcher {
}
}
fun patch(options: Options) {
LSPatch(*options.toStringArray()).doCommandLine()
suspend fun patch(logger: Logger, options: Options) {
withContext(Dispatchers.IO) {
LSPatch(logger, *options.toStringArray()).doCommandLine()
}
}
}

View File

@ -0,0 +1,73 @@
package org.lsposed.lspatch.ui.component
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
private val ShimmerColorShades
@Composable get() = listOf(
MaterialTheme.colorScheme.secondaryContainer.copy(0.9f),
MaterialTheme.colorScheme.secondaryContainer.copy(0.2f),
MaterialTheme.colorScheme.secondaryContainer.copy(0.9f)
)
class ShimmerScope(val brush: Brush)
@Composable
fun ShimmerAnimation(
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable ShimmerScope.() -> 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)
)
}
}
}

View File

@ -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<AppInfo?>(null)
var confirm by mutableStateOf(false)
var patchState by mutableStateOf(PatchState.SELECTING)
var patchOptions by mutableStateOf<Patcher.Options?>(null)
}
@Composable
fun NewPatchFab() {
val viewModel = viewModel<NewPatchPageViewModel>()
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<NewPatchPageViewModel>()
val navController = LocalNavController.current
val appInfo by navController.currentBackStackEntry!!.savedStateHandle
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
.getLiveData<AppInfo>("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<NewPatchPageViewModel>()
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<NewPatchPageViewModel>()
val navController = LocalNavController.current
val logs = remember { mutableStateListOf<Pair<Int, String>>() }
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)) }
)
}
}
}
}

View File

@ -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

View File

@ -26,4 +26,6 @@
<string name="patch_override_version_code">Override version code</string>
<string name="patch_override_version_code_desc">Override the patched app\'s version code to 1\nThis allows downgrade installation</string>
<string name="patch_start">Start Patch</string>
<string name="patch_return">Return</string>
<string name="patch_install">Install</string>
</resources>

View File

@ -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<String> 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.");
}
}
}

View File

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

View File

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