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" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"

View File

@ -1,6 +1,9 @@
package org.lsposed.lspatch package org.lsposed.lspatch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.lsposed.patch.LSPatch import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
object Patcher { object Patcher {
class Options( class Options(
@ -38,7 +41,9 @@ object Patcher {
} }
} }
fun patch(options: Options) { suspend fun patch(logger: Logger, options: Options) {
LSPatch(*options.toStringArray()).doCommandLine() 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 package org.lsposed.lspatch.ui.page
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Spacer import androidx.compose.animation.core.Spring
import androidx.compose.foundation.layout.height import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.padding 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Api 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.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.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.R
import org.lsposed.lspatch.TAG import org.lsposed.lspatch.TAG
import org.lsposed.lspatch.ui.component.SelectionColumn 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.SettingsCheckBox
import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.util.LocalNavController 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.lspatch.ui.viewmodel.AppInfo
import org.lsposed.patch.util.Logger
enum class PatchState {
SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED
}
class NewPatchPageViewModel : ViewModel() { class NewPatchPageViewModel : ViewModel() {
var patchApp by mutableStateOf<AppInfo?>(null) var patchState by mutableStateOf(PatchState.SELECTING)
var confirm by mutableStateOf(false)
var patchOptions by mutableStateOf<Patcher.Options?>(null) var patchOptions by mutableStateOf<Patcher.Options?>(null)
} }
@Composable @Composable
fun NewPatchFab() { fun NewPatchFab() {
val viewModel = viewModel<NewPatchPageViewModel>() val viewModel = viewModel<NewPatchPageViewModel>()
if (viewModel.patchApp != null) { if (viewModel.patchState == PatchState.CONFIGURING) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch_start)) }, text = { Text(stringResource(R.string.patch_start)) },
icon = { Icon(Icons.Outlined.AutoFixHigh, null) }, icon = { Icon(Icons.Outlined.AutoFixHigh, null) },
onClick = { viewModel.confirm = true } onClick = { viewModel.patchState = PatchState.SUBMITTING }
) )
} }
} }
@ -53,21 +68,20 @@ fun NewPatchFab() {
fun NewPatchPage() { fun NewPatchPage() {
val viewModel = viewModel<NewPatchPageViewModel>() val viewModel = viewModel<NewPatchPageViewModel>()
val navController = LocalNavController.current val navController = LocalNavController.current
val appInfo by navController.currentBackStackEntry!!.savedStateHandle val patchApp by navController.currentBackStackEntry!!.savedStateHandle
.getLiveData<AppInfo>("appInfo").observeAsState() .getLiveData<AppInfo>("appInfo").observeAsState()
viewModel.patchApp = appInfo if (viewModel.patchState == PatchState.SELECTING && patchApp != null) viewModel.patchState = PatchState.CONFIGURING
Log.d(TAG, "confirm = ${viewModel.confirm}") Log.d(TAG, "NewPatchPage: ${viewModel.patchState}")
when (viewModel.patchState) {
when { PatchState.SELECTING -> navController.navigate(PageList.SelectApps.name + "/false")
viewModel.patchApp == null -> navController.navigate(PageList.SelectApps.name + "/false") PatchState.CONFIGURING, PatchState.SUBMITTING -> PatchOptionsPage(patchApp!!)
viewModel.patchOptions == null -> PatchOptionsPage(viewModel.patchApp!!, viewModel.confirm) PatchState.PATCHING, PatchState.FINISHED -> DoPatchPage(viewModel.patchOptions!!)
else -> PatchingPage(viewModel.patchOptions!!)
} }
} }
@Composable @Composable
private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) { private fun PatchOptionsPage(patchApp: AppInfo) {
val viewModel = viewModel<NewPatchPageViewModel>() val viewModel = viewModel<NewPatchPageViewModel>()
var useManager by rememberSaveable { mutableStateOf(true) } var useManager by rememberSaveable { mutableStateOf(true) }
var debuggable by rememberSaveable { mutableStateOf(false) } var debuggable by rememberSaveable { mutableStateOf(false) }
@ -77,9 +91,11 @@ private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) {
val sigBypassLevel by rememberSaveable { mutableStateOf(2) } val sigBypassLevel by rememberSaveable { mutableStateOf(2) }
var overrideVersionCode by rememberSaveable { mutableStateOf(false) } 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( viewModel.patchOptions = Patcher.Options(
apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk
outputPath = downloadDir,
debuggable = debuggable, debuggable = debuggable,
sigbypassLevel = sigBypassLevel, sigbypassLevel = sigBypassLevel,
v1 = v1, v2 = v2, v3 = v3, v1 = v1, v2 = v2, v3 = v3,
@ -88,6 +104,7 @@ private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) {
verbose = true, verbose = true,
embeddedModules = emptyList() // TODO: Embed modules embeddedModules = emptyList() // TODO: Embed modules
) )
viewModel.patchState = PatchState.PATCHING
} }
Column( Column(
@ -172,6 +189,89 @@ private fun PatchOptionsPage(patchApp: AppInfo, confirm: Boolean) {
} }
@Composable @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">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_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_start">Start Patch</string>
<string name="patch_return">Return</string>
<string name="patch_install">Install</string>
</resources> </resources>

View File

@ -23,6 +23,8 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.lsposed.lspatch.share.PatchConfig; import org.lsposed.lspatch.share.PatchConfig;
import org.lsposed.patch.util.ApkSignatureHelper; 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 org.lsposed.patch.util.ManifestParser;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -114,15 +116,19 @@ public class LSPatch {
private final JCommander jCommander; private final JCommander jCommander;
public LSPatch(String... args) { private final Logger logger;
public LSPatch(Logger logger, String... args) {
jCommander = JCommander.newBuilder() jCommander = JCommander.newBuilder()
.addObject(this) .addObject(this)
.build(); .build();
jCommander.parse(args); jCommander.parse(args);
this.logger = logger;
logger.verbose = verbose;
} }
public static void main(String... args) throws IOException { public static void main(String... args) throws IOException {
LSPatch lsPatch = new LSPatch(args); LSPatch lsPatch = new LSPatch(new JavaLogger(), args);
try { try {
lsPatch.doCommandLine(); lsPatch.doCommandLine();
} catch (PatchError e) { } catch (PatchError e) {
@ -157,7 +163,7 @@ public class LSPatch {
if (outputFile.exists() && !forceOverwrite) if (outputFile.exists() && !forceOverwrite)
throw new PatchError(outputPath + " exists. Use --force to overwrite"); throw new PatchError(outputPath + " exists. Use --force to overwrite");
System.out.println("Processing " + srcApkFile + " -> " + outputFile); logger.i("Processing " + srcApkFile + " -> " + outputFile);
patch(srcApkFile, outputFile); patch(srcApkFile, outputFile);
} }
@ -170,16 +176,15 @@ public class LSPatch {
File tmpApk = Files.createTempFile(srcApkFile.getName(), "unsigned").toFile(); File tmpApk = Files.createTempFile(srcApkFile.getName(), "unsigned").toFile();
tmpApk.delete(); tmpApk.delete();
if (verbose) logger.d("apk path: " + srcApkFile);
System.out.println("apk path: " + srcApkFile);
System.out.println("Parsing original apk..."); logger.i("Parsing original apk...");
try (var dstZFile = ZFile.openReadWrite(tmpApk, Z_FILE_OPTIONS); try (var dstZFile = ZFile.openReadWrite(tmpApk, Z_FILE_OPTIONS);
var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) { var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) {
// sign apk // sign apk
System.out.println("Register apk signer..."); logger.i("Register apk signer...");
try { try {
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) { try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) {
@ -205,8 +210,7 @@ public class LSPatch {
throw new PatchError("get original signature failed"); throw new PatchError("get original signature failed");
} }
if (verbose) logger.d("Original signature\n" + originalSignature);
System.out.println("Original signature\n" + originalSignature);
} }
// copy out manifest file from zlib // copy out manifest file from zlib
@ -222,11 +226,10 @@ public class LSPatch {
throw new PatchError("Failed to parse AndroidManifest.xml"); throw new PatchError("Failed to parse AndroidManifest.xml");
appComponentFactory = pair.appComponentFactory == null ? "" : pair.appComponentFactory; appComponentFactory = pair.appComponentFactory == null ? "" : pair.appComponentFactory;
if (verbose) logger.d("original appComponentFactory class: " + appComponentFactory);
System.out.println("original appComponentFactory class: " + appComponentFactory);
} }
System.out.println("Patching apk..."); logger.i("Patching apk...");
// modify manifest // modify manifest
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open()))) { try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open()))) {
dstZFile.add(ANDROID_MANIFEST_XML, is); dstZFile.add(ANDROID_MANIFEST_XML, is);
@ -234,8 +237,7 @@ public class LSPatch {
throw new PatchError("Error when modifying manifest", e); throw new PatchError("Error when modifying manifest", e);
} }
if (verbose) logger.d("Adding native lib..");
System.out.println("Adding native lib..");
// copy so and dex files into the unzipped apk // copy so and dex files into the unzipped apk
// do not put liblspd.so into apk!lib because x86 native bridge causes crash // do not put liblspd.so into apk!lib because x86 native bridge causes crash
@ -247,12 +249,10 @@ public class LSPatch {
// More exception info // More exception info
throw new PatchError("Error when adding native lib", e); throw new PatchError("Error when adding native lib", e);
} }
if (verbose) logger.d("added " + entryName);
System.out.println("added " + entryName);
} }
if (verbose) logger.d("Adding dex..");
System.out.println("Adding dex..");
try (var is = getClass().getClassLoader().getResourceAsStream("assets/dex/loader.dex")) { try (var is = getClass().getClassLoader().getResourceAsStream("assets/dex/loader.dex")) {
dstZFile.add("classes.dex", is); dstZFile.add("classes.dex", is);
@ -278,8 +278,7 @@ public class LSPatch {
Set<String> apkArchs = new HashSet<>(); Set<String> apkArchs = new HashSet<>();
if (verbose) logger.d("Search target apk library arch...");
System.out.println("Search target apk library arch...");
for (StoredEntry storedEntry : srcZFile.entries()) { for (StoredEntry storedEntry : srcZFile.entries()) {
var name = storedEntry.getCentralDirectoryHeader().getName(); var name = storedEntry.getCentralDirectoryHeader().getName();
@ -291,7 +290,7 @@ public class LSPatch {
if (apkArchs.isEmpty()) apkArchs.addAll(ARCHES); if (apkArchs.isEmpty()) apkArchs.addAll(ARCHES);
apkArchs.removeIf((arch) -> { apkArchs.removeIf((arch) -> {
if (!ARCHES.contains(arch) && !arch.equals("armeabi")) { 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 true;
} }
return false; return false;
@ -300,8 +299,7 @@ public class LSPatch {
embedModules(dstZFile); embedModules(dstZFile);
// create zip link // create zip link
if (verbose) logger.d("Creating nested apk link...");
System.out.println("Creating nested apk link...");
for (StoredEntry entry : srcZFile.entries()) { for (StoredEntry entry : srcZFile.entries()) {
String name = entry.getCentralDirectoryHeader().getName(); String name = entry.getCentralDirectoryHeader().getName();
@ -314,12 +312,12 @@ public class LSPatch {
dstZFile.realign(); dstZFile.realign();
System.out.println("Writing apk..."); logger.i("Writing apk...");
} finally { } finally {
try { try {
outputFile.delete(); outputFile.delete();
FileUtils.moveFile(tmpApk, outputFile); FileUtils.moveFile(tmpApk, outputFile);
System.out.println("Done. Output APK: " + outputFile.getAbsolutePath()); logger.i("Done. Output APK: " + outputFile.getAbsolutePath());
} catch (Throwable e) { } catch (Throwable e) {
throw new PatchError("Error writing apk", e); throw new PatchError("Error writing apk", e);
} }
@ -327,7 +325,7 @@ public class LSPatch {
} }
private void embedModules(ZFile zFile) { private void embedModules(ZFile zFile) {
System.out.println("Embedding modules..."); logger.i("Embedding modules...");
for (var module : modules) { for (var module : modules) {
File file = new File(module); File file = new File(module);
try (var apk = ZFile.openReadOnly(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 manifest = Objects.requireNonNull(ManifestParser.parseManifestFile(xmlIs));
var packageName = manifest.packageName; var packageName = manifest.packageName;
System.out.println(" - " + packageName); logger.i(" - " + packageName);
zFile.add("assets/lspatch/modules/" + packageName + ".bin", fileIs); zFile.add("assets/lspatch/modules/" + packageName + ".bin", fileIs);
} catch (NullPointerException | IOException e) { } 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);
}