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 { defaultConfig {
applicationId "com.zane.smapiinstaller" applicationId "com.zane.smapiinstaller"
minSdkVersion 19 minSdkVersion 21
//noinspection ExpiringTargetSdkVersion //noinspection ExpiringTargetSdkVersion
targetSdkVersion 30 targetSdkVersion 30
versionCode 73 versionCode 73

View File

@ -13,13 +13,11 @@ import android.util.Log;
import com.android.apksig.ApkSigner; import com.android.apksig.ApkSigner;
import com.android.apksig.ApkVerifier; import com.android.apksig.ApkVerifier;
import com.android.apksig.DefaultApkSignerEngine; import com.android.apksig.DefaultApkSignerEngine;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSources; import com.android.apksig.util.DataSources;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker; import com.google.common.base.Ticker;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.zane.smapiinstaller.BuildConfig; import com.zane.smapiinstaller.BuildConfig;
import com.zane.smapiinstaller.MainActivity; import com.zane.smapiinstaller.MainActivity;
import com.zane.smapiinstaller.R; 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.entity.ManifestEntry;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils; import com.zane.smapiinstaller.utils.FileUtils;
import com.zane.smapiinstaller.utils.PackageInstallUtil;
import com.zane.smapiinstaller.utils.StringUtils; import com.zane.smapiinstaller.utils.StringUtils;
import com.zane.smapiinstaller.utils.ZipUtils; import com.zane.smapiinstaller.utils.ZipUtils;
@ -49,6 +48,7 @@ import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -173,11 +173,12 @@ public class ApkPatcher {
* 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确 * 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确
* *
* @param apkPath APK文件路径 * @param apkPath APK文件路径
* @param second
* @param targetFile 目标文件 * @param targetFile 目标文件
* @param isAdvanced 是否高级模式 * @param isAdvanced 是否高级模式
* @return 是否成功打包 * @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) { if (apkPath == null) {
return false; return false;
} }
@ -210,7 +211,7 @@ public class ApkPatcher {
int baseProgress = 10; int baseProgress = 10;
stopwatch.reset(); stopwatch.reset();
stopwatch.start(); 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")), (entryName) -> entryName.startsWith("assemblies/assemblies.") || (isAdvanced && entryName.startsWith("assets/Content")),
(progress) -> emitProgress((int) (baseProgress + (progress / 100.0) * 35))); (progress) -> emitProgress((int) (baseProgress + (progress / 100.0) * 35)));
stopwatch.stop(); stopwatch.stop();
@ -462,7 +463,7 @@ public class ApkPatcher {
if (thread.isAlive() && !thread.isInterrupted()) { if (thread.isAlive() && !thread.isInterrupted()) {
thread.interrupt(); thread.interrupt();
} }
if (result.containsErrors()) { if (result.containsErrors() && result.getErrors().size() > 0) {
errorMessage.set(result.getErrors().stream().map(ApkVerifier.IssueWithParams::toString).collect(Collectors.joining(","))); errorMessage.set(result.getErrors().stream().map(ApkVerifier.IssueWithParams::toString).collect(Collectors.joining(",")));
return null; return null;
} }
@ -478,7 +479,7 @@ public class ApkPatcher {
/** /**
* 对指定安装包发起安装 * 对指定安装包发起安装
* *
* @param apkPath 安装包路径 * @param apkPath 安装包路径
*/ */
public void install(String apkPath) { public void install(String apkPath) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 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.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.common.collect.Lists;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.zane.smapiinstaller.MainApplication; import com.zane.smapiinstaller.MainApplication;
import com.zane.smapiinstaller.R; 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.constant.DialogAction;
import com.zane.smapiinstaller.databinding.FragmentInstallBinding; import com.zane.smapiinstaller.databinding.FragmentInstallBinding;
import com.zane.smapiinstaller.dto.Tuple2; import com.zane.smapiinstaller.dto.Tuple2;
import com.zane.smapiinstaller.logic.ActivityResultHandler;
import com.zane.smapiinstaller.logic.ApkPatcher; import com.zane.smapiinstaller.logic.ApkPatcher;
import com.zane.smapiinstaller.logic.CommonLogic; import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ModAssetsManager;
import com.zane.smapiinstaller.ui.main.MainTabsFragmentDirections; import com.zane.smapiinstaller.ui.main.MainTabsFragmentDirections;
import com.zane.smapiinstaller.utils.ConfigUtils; import com.zane.smapiinstaller.utils.ConfigUtils;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
@ -151,7 +148,7 @@ public class InstallFragment extends Fragment {
// modAssetsManager.installDefaultMods(); // modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8);
File targetApk = new File(dest, "base.apk"); 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); int target = patcher.getSwitchAction().getAndSet(0);
if (target == R.string.menu_download) { 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) -> { 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; return;
} }
List<String> resourcePacks = new ArrayList<>(); // List<String> resourcePacks = new ArrayList<>();
if (paths.getSecond() != null) { // if (paths.getSecond() != null) {
for (String resourcePack : paths.getSecond()) { // for (String resourcePack : paths.getSecond()) {
File targetResourcePack = new File(dest, FilenameUtils.getName(resourcePack)); // File targetResourcePack = new File(dest, FilenameUtils.getName(resourcePack));
patcher.patch(resourcePack, targetResourcePack, false, true); // patcher.patch(resourcePack, paths.getSecond(), targetResourcePack, false, true);
resourcePacks.add(targetResourcePack.getAbsolutePath()); // resourcePacks.add(targetResourcePack.getAbsolutePath());
} // }
} // }
DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null);
String signPath = patcher.sign(targetApk.getAbsolutePath()); String signPath = patcher.sign(targetApk.getAbsolutePath());
if (signPath == null) { if (signPath == null) {
DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game))); DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return; 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); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.installing_package, null);
patcher.install(signPath); 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; 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 inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile(); File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) { if (inFile.equals(outFile)) {
@ -127,11 +127,11 @@ public class ZipUtils {
ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath); ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath);
byte[] originManifest = null; byte[] originManifest = null;
ConcurrentHashMap<String, Boolean> originEntryName = new ConcurrentHashMap<>(); ConcurrentHashMap<String, Boolean> originEntryName = new ConcurrentHashMap<>();
try (ZipInput input = new ZipInput(inputZipFilename)) { try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
int size = input.entries.values().size(); try (ZipInput input = new ZipInput(inputZipFilename)) {
AtomicLong count = new AtomicLong(); int size = input.entries.values().size();
int reportInterval = size / 100; AtomicLong count = new AtomicLong();
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) { int reportInterval = size / 100;
ConcurrentHashMap<String, Boolean> replacedFileSet = new ConcurrentHashMap<>(entryMap.size()); ConcurrentHashMap<String, Boolean> replacedFileSet = new ConcurrentHashMap<>(entryMap.size());
MultiprocessingUtil.TaskBundle<ZioEntry> taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> { MultiprocessingUtil.TaskBundle<ZioEntry> taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> {
try { try {
@ -200,6 +200,22 @@ public class ZipUtils {
taskBundle.join(); taskBundle.join();
progressCallback.accept(100); 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) { } catch (RuntimeException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) { if (e.getCause() != null && e.getCause() instanceof IOException) {
throw (IOException) e.getCause(); throw (IOException) e.getCause();