Alter install logic to build single giant apk

This commit is contained in:
zhiyang7 2023-03-08 01:01:12 +08:00
parent a6c21a83a8
commit 02ab9856f4
5 changed files with 102 additions and 26 deletions

View File

@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId "com.zane.smapiinstaller"
minSdkVersion 19
minSdkVersion 21
//noinspection ExpiringTargetSdkVersion
targetSdkVersion 30
versionCode 73

View File

@ -13,13 +13,11 @@ import android.util.Log;
import com.android.apksig.ApkSigner;
import com.android.apksig.ApkVerifier;
import com.android.apksig.DefaultApkSignerEngine;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSources;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.zane.smapiinstaller.BuildConfig;
import com.zane.smapiinstaller.MainActivity;
import com.zane.smapiinstaller.R;
@ -31,6 +29,7 @@ import com.zane.smapiinstaller.entity.ApkFilesManifest;
import com.zane.smapiinstaller.entity.ManifestEntry;
import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils;
import com.zane.smapiinstaller.utils.PackageInstallUtil;
import com.zane.smapiinstaller.utils.StringUtils;
import com.zane.smapiinstaller.utils.ZipUtils;
@ -49,6 +48,7 @@ import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -173,11 +173,12 @@ public class ApkPatcher {
* 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确
*
* @param apkPath APK文件路径
* @param second
* @param targetFile 目标文件
* @param isAdvanced 是否高级模式
* @return 是否成功打包
*/
public boolean patch(String apkPath, File targetFile, boolean isAdvanced, boolean isResourcePack) {
public boolean patch(String apkPath, String[] resourcePacks, File targetFile, boolean isAdvanced, boolean isResourcePack) {
if (apkPath == null) {
return false;
}
@ -210,7 +211,7 @@ public class ApkPatcher {
int baseProgress = 10;
stopwatch.reset();
stopwatch.start();
originSignInfo = ZipUtils.addOrReplaceEntries(apkPath, entries, targetFile.getAbsolutePath(),
originSignInfo = ZipUtils.addOrReplaceEntries(apkPath, resourcePacks, entries, targetFile.getAbsolutePath(),
(entryName) -> entryName.startsWith("assemblies/assemblies.") || (isAdvanced && entryName.startsWith("assets/Content")),
(progress) -> emitProgress((int) (baseProgress + (progress / 100.0) * 35)));
stopwatch.stop();
@ -462,7 +463,7 @@ public class ApkPatcher {
if (thread.isAlive() && !thread.isInterrupted()) {
thread.interrupt();
}
if (result.containsErrors()) {
if (result.containsErrors() && result.getErrors().size() > 0) {
errorMessage.set(result.getErrors().stream().map(ApkVerifier.IssueWithParams::toString).collect(Collectors.joining(",")));
return null;
}
@ -478,7 +479,7 @@ public class ApkPatcher {
/**
* 对指定安装包发起安装
*
* @param apkPath 安装包路径
* @param apkPath 安装包路径
*/
public void install(String apkPath) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {

View File

@ -7,7 +7,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.zane.smapiinstaller.MainApplication;
import com.zane.smapiinstaller.R;
@ -16,10 +15,8 @@ import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.databinding.FragmentInstallBinding;
import com.zane.smapiinstaller.dto.Tuple2;
import com.zane.smapiinstaller.logic.ActivityResultHandler;
import com.zane.smapiinstaller.logic.ApkPatcher;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ModAssetsManager;
import com.zane.smapiinstaller.ui.main.MainTabsFragmentDirections;
import com.zane.smapiinstaller.utils.ConfigUtils;
import com.zane.smapiinstaller.utils.DialogUtils;
@ -151,7 +148,7 @@ public class InstallFragment extends Fragment {
// modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8);
File targetApk = new File(dest, "base.apk");
if (!patcher.patch(paths.getFirst(), targetApk, isAdv, false)) {
if (!patcher.patch(paths.getFirst(), paths.getSecond(), targetApk, isAdv, false)) {
int target = patcher.getSwitchAction().getAndSet(0);
if (target == R.string.menu_download) {
DialogUtils.showConfirmDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)), R.string.menu_download, R.string.cancel, (d, which) -> {
@ -165,21 +162,21 @@ public class InstallFragment extends Fragment {
}
return;
}
List<String> resourcePacks = new ArrayList<>();
if (paths.getSecond() != null) {
for (String resourcePack : paths.getSecond()) {
File targetResourcePack = new File(dest, FilenameUtils.getName(resourcePack));
patcher.patch(resourcePack, targetResourcePack, false, true);
resourcePacks.add(targetResourcePack.getAbsolutePath());
}
}
// List<String> resourcePacks = new ArrayList<>();
// if (paths.getSecond() != null) {
// for (String resourcePack : paths.getSecond()) {
// File targetResourcePack = new File(dest, FilenameUtils.getName(resourcePack));
// patcher.patch(resourcePack, paths.getSecond(), targetResourcePack, false, true);
// resourcePacks.add(targetResourcePack.getAbsolutePath());
// }
// }
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null);
String signPath = patcher.sign(targetApk.getAbsolutePath());
if (signPath == null) {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return;
}
List<String> signedResourcePacks = resourcePacks.stream().map(patcher::sign).collect(Collectors.toList());
// List<String> signedResourcePacks = resourcePacks.stream().map(patcher::sign).collect(Collectors.toList());
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.installing_package, null);
patcher.install(signPath);
}));

View File

@ -0,0 +1,62 @@
package com.zane.smapiinstaller.utils;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageInstaller;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
public class PackageInstallUtil {
public static final String ACTION_INSTALL_COMPLETE = "com.zane.smapiinstaller.INSTALL_COMPLETE";
public static boolean installPackage(Context context, String packageName, String apkPath, List<String> signedResourcePacks) {
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_INHERIT_EXISTING);
params.setAppPackageName(packageName);
try {
// set params
int sessionId = packageInstaller.createSession(params);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
writePackage(session, apkPath);
for (String pack : signedResourcePacks) {
writePackage(session, pack);
}
session.commit(createIntentSender(context, sessionId));
return true;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void writePackage(PackageInstaller.Session session, String apkPath) throws IOException {
File file = new File(apkPath);
try(FileInputStream in = new FileInputStream(file)) {
OutputStream out = session.openWrite(file.getName(), 0, -1);
byte[] buffer = new byte[65536];
int c;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
session.fsync(out);
out.close();
}
}
private static IntentSender createIntentSender(Context context, int sessionId) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
new Intent(ACTION_INSTALL_COMPLETE),
0);
return pendingIntent.getIntentSender();
}
}

View File

@ -118,7 +118,7 @@ public class ZipUtils {
return result;
}
public static Tuple2<byte[], Set<String>> addOrReplaceEntries(String inputZipFilename, List<ZipEntrySource> entrySources, String outputZipFilename, Function<String, Boolean> removePredict, Consumer<Integer> progressCallback) throws IOException {
public static Tuple2<byte[], Set<String>> addOrReplaceEntries(String inputZipFilename, String[] resourcePacks, List<ZipEntrySource> entrySources, String outputZipFilename, Function<String, Boolean> removePredict, Consumer<Integer> progressCallback) throws IOException {
File inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) {
@ -127,11 +127,11 @@ public class ZipUtils {
ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath);
byte[] originManifest = null;
ConcurrentHashMap<String, Boolean> originEntryName = new ConcurrentHashMap<>();
try (ZipInput input = new ZipInput(inputZipFilename)) {
int size = input.entries.values().size();
AtomicLong count = new AtomicLong();
int reportInterval = size / 100;
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
try (ZipInput input = new ZipInput(inputZipFilename)) {
int size = input.entries.values().size();
AtomicLong count = new AtomicLong();
int reportInterval = size / 100;
ConcurrentHashMap<String, Boolean> replacedFileSet = new ConcurrentHashMap<>(entryMap.size());
MultiprocessingUtil.TaskBundle<ZioEntry> taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> {
try {
@ -200,6 +200,22 @@ public class ZipUtils {
taskBundle.join();
progressCallback.accept(100);
}
for (String resourcePack : resourcePacks) {
try (ZipInput input = new ZipInput(resourcePack)) {
for (ZioEntry inEntry : input.entries.values()) {
if(inEntry.getName().startsWith("assets/Content")) {
ZioEntry zioEntry = new ZioEntry(inEntry.getName());
zioEntry.setCompression(inEntry.getCompression());
try (InputStream inputStream = inEntry.getInputStream()) {
ByteStreams.copy(inputStream, zioEntry.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
zipOutput.write(zioEntry);
}
}
}
}
} catch (RuntimeException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw (IOException) e.getCause();