Alter install logic to build single giant apk
This commit is contained in:
parent
a6c21a83a8
commit
02ab9856f4
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue