From 02ab9856f49e640055efcc54decf43e055bda00e Mon Sep 17 00:00:00 2001 From: zhiyang7 Date: Wed, 8 Mar 2023 01:01:12 +0800 Subject: [PATCH] Alter install logic to build single giant apk --- app/build.gradle | 2 +- .../zane/smapiinstaller/logic/ApkPatcher.java | 13 ++-- .../ui/install/InstallFragment.java | 23 +++---- .../utils/PackageInstallUtil.java | 62 +++++++++++++++++++ .../zane/smapiinstaller/utils/ZipUtils.java | 28 +++++++-- 5 files changed, 102 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/zane/smapiinstaller/utils/PackageInstallUtil.java diff --git a/app/build.gradle b/app/build.gradle index 7a5f953..b040c5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.zane.smapiinstaller" - minSdkVersion 19 + minSdkVersion 21 //noinspection ExpiringTargetSdkVersion targetSdkVersion 30 versionCode 73 diff --git a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java index c062977..ca44213 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java @@ -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) { diff --git a/app/src/main/java/com/zane/smapiinstaller/ui/install/InstallFragment.java b/app/src/main/java/com/zane/smapiinstaller/ui/install/InstallFragment.java index b17cea3..aa37924 100644 --- a/app/src/main/java/com/zane/smapiinstaller/ui/install/InstallFragment.java +++ b/app/src/main/java/com/zane/smapiinstaller/ui/install/InstallFragment.java @@ -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 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 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 signedResourcePacks = resourcePacks.stream().map(patcher::sign).collect(Collectors.toList()); +// List signedResourcePacks = resourcePacks.stream().map(patcher::sign).collect(Collectors.toList()); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.installing_package, null); patcher.install(signPath); })); diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/PackageInstallUtil.java b/app/src/main/java/com/zane/smapiinstaller/utils/PackageInstallUtil.java new file mode 100644 index 0000000..7334d8b --- /dev/null +++ b/app/src/main/java/com/zane/smapiinstaller/utils/PackageInstallUtil.java @@ -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 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(); + } +} diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java b/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java index aa82cf0..77b2546 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java @@ -118,7 +118,7 @@ public class ZipUtils { return result; } - public static Tuple2> addOrReplaceEntries(String inputZipFilename, List entrySources, String outputZipFilename, Function removePredict, Consumer progressCallback) throws IOException { + public static Tuple2> addOrReplaceEntries(String inputZipFilename, String[] resourcePacks, List entrySources, String outputZipFilename, Function removePredict, Consumer 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 entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath); byte[] originManifest = null; ConcurrentHashMap 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 replacedFileSet = new ConcurrentHashMap<>(entryMap.size()); MultiprocessingUtil.TaskBundle 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();