1.Improve patch processing speed

2.Make process bar more accurately
This commit is contained in:
ZaneYork 2020-03-25 21:26:54 +08:00
parent ebb36e9388
commit 27d7ba816a
7 changed files with 190 additions and 75 deletions

View File

@ -23,11 +23,11 @@ android {
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
// debug {
// minifyEnabled true
// shrinkResources true
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// }
}
compileOptions {
@ -57,8 +57,8 @@ dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment:2.3.0-alpha03'
implementation 'androidx.navigation:navigation-ui:2.3.0-alpha03'
implementation 'androidx.navigation:navigation-fragment:2.3.0-alpha04'
implementation 'androidx.navigation:navigation-ui:2.3.0-alpha04'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.madgag.spongycastle:core:1.54.0.0'
implementation 'com.madgag.spongycastle:prov:1.54.0.0'

View File

@ -1,6 +1,7 @@
package com.zane.smapiinstaller.entity;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import java.util.Set;
import lombok.Data;
@ -9,6 +10,7 @@ import lombok.Data;
* Mod信息
*/
@Data
@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY, getterVisibility= JsonAutoDetect.Visibility.NONE)
public class ModManifestEntry {
/**
* 存放位置

View File

@ -13,6 +13,7 @@ import android.util.Log;
import com.android.apksig.ApkSigner;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Predicate;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import com.zane.smapiinstaller.BuildConfig;
@ -22,13 +23,11 @@ import com.zane.smapiinstaller.constant.ManifestPatchConstants;
import com.zane.smapiinstaller.entity.ApkFilesManifest;
import com.zane.smapiinstaller.entity.ManifestEntry;
import com.zane.smapiinstaller.utils.FileUtils;
import com.zane.smapiinstaller.utils.ZipUtils;
import net.fornwall.apksigner.KeyStoreFileManager;
import net.fornwall.apksigner.ZipAligner;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.ByteSource;
import org.zeroturnaround.zip.ZipEntrySource;
import org.zeroturnaround.zip.ZipUtil;
import java.io.File;
@ -40,12 +39,18 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java9.util.function.Consumer;
import java9.util.stream.Collectors;
import java.util.zip.Deflater;
import androidx.core.content.FileProvider;
import java9.util.stream.StreamSupport;
import pxb.android.axml.NodeVisitor;
public class ApkPatcher {
@ -60,21 +65,29 @@ public class ApkPatcher {
private AtomicInteger switchAction = new AtomicInteger();
private List<Consumer<Integer>> progressListener = new ArrayList<>();
private Stopwatch stopwatch = Stopwatch.createUnstarted();
public ApkPatcher(Context context) {
this.context = context;
}
/**
* 依次扫描package_names.json文件对应的包名抽取找到的第一个游戏APK到SMAPI Installer路径
*
* @return 抽取后的APK文件路径如果抽取失败返回null
*/
public String extract() {
emitProgress(0);
PackageManager packageManager = context.getPackageManager();
List<String> packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference<List<String>>() { });
List<String> packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference<List<String>>() {
});
if (packageNames == null) {
errorMessage.set(context.getString(R.string.error_game_not_found));
return null;
}
emitProgress(1);
for (String packageName : packageNames) {
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
@ -92,6 +105,7 @@ public class ApkPatcher {
}
File distFile = new File(dest, apkFile.getName());
Files.copy(apkFile, distFile);
emitProgress(5);
return distFile.getAbsolutePath();
}
} catch (PackageManager.NameNotFoundException | IOException e) {
@ -104,6 +118,7 @@ public class ApkPatcher {
/**
* 将指定APK文件重新打包添加SMAPI修改AndroidManifest.xml同时验证版本是否正确
*
* @param apkPath APK文件路径
* @return 是否成功打包
*/
@ -116,31 +131,43 @@ public class ApkPatcher {
return false;
}
try {
List<ZipEntrySource> zipEntrySourceList = new ArrayList<>();
byte[] manifest = ZipUtil.unpackEntry(file, "AndroidManifest.xml");
emitProgress(9);
List<ApkFilesManifest> apkFilesManifests = CommonLogic.findAllApkFileManifest(context);
byte[] modifiedManifest = modifyManifest(manifest, apkFilesManifests);
if(apkFilesManifests.size() == 0) {
if (apkFilesManifests.size() == 0) {
errorMessage.set(context.getString(R.string.error_no_supported_game_version));
switchAction.set(R.string.menu_download);
return false;
}
if(modifiedManifest == null) {
if (modifiedManifest == null) {
errorMessage.set(context.getString(R.string.failed_to_process_manifest));
return false;
}
zipEntrySourceList.add(new ByteSource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
ApkFilesManifest apkFilesManifest = apkFilesManifests.get(0);
List<ManifestEntry> manifestEntries = apkFilesManifest.getManifestEntries();
for (ManifestEntry entry : manifestEntries) {
if(entry.isExternal()) {
zipEntrySourceList.add(new ByteSource(entry.getTargetPath(), FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath()), entry.getCompression()));
List<ZipUtils.ZipEntrySource> entries = StreamSupport.stream(manifestEntries).map(entry -> {
if (entry.isExternal()) {
return new ZipUtils.ZipEntrySource(entry.getTargetPath(), FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath()), entry.getCompression());
} else {
return new ZipUtils.ZipEntrySource(entry.getTargetPath(), FileUtils.getAssetBytes(context, entry.getAssetPath()), entry.getCompression());
}
else {
zipEntrySourceList.add(new ByteSource(entry.getTargetPath(), FileUtils.getAssetBytes(context, entry.getAssetPath()), entry.getCompression()));
}
}
ZipUtil.addOrReplaceEntries(file, zipEntrySourceList.toArray(new ZipEntrySource[0]));
}).collect(Collectors.toList());
entries.add(new ZipUtils.ZipEntrySource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
emitProgress(10);
String patchedFilename = apkPath + ".patched";
File patchedFile = new File(patchedFilename);
int baseProgress = 10;
stopwatch.reset();
stopwatch.start();
ZipUtils.addOrReplaceEntries(apkPath, entries, patchedFilename, (progress)->{
emitProgress((int) (baseProgress + (progress / 100.0) * 35));
});
stopwatch.stop();
emitProgress(45);
FileUtils.forceDelete(file);
FileUtils.moveFile(patchedFile, file);
emitProgress(46);
return true;
} catch (Exception e) {
Log.e(TAG, "Patch error", e);
@ -151,7 +178,8 @@ public class ApkPatcher {
/**
* 扫描全部兼容包寻找匹配的版本修改AndroidManifest.xml文件
* @param bytes AndroidManifest.xml的字节数组
*
* @param bytes AndroidManifest.xml的字节数组
* @param manifests 兼容包列表
* @return 修改后的AndroidManifest.xml的字节数组
*/
@ -176,8 +204,7 @@ public class ApkPatcher {
case "authorities":
if (strObj.contains(packageName.get())) {
attr.obj = strObj.replace(packageName.get(), Constants.TARGET_PACKAGE_NAME);
}
else if(strObj.contains(ManifestPatchConstants.APP_PACKAGE_NAME)){
} else if (strObj.contains(ManifestPatchConstants.APP_PACKAGE_NAME)) {
attr.obj = strObj.replace(ManifestPatchConstants.APP_PACKAGE_NAME, Constants.TARGET_PACKAGE_NAME);
}
case "name":
@ -188,9 +215,8 @@ public class ApkPatcher {
default:
break;
}
}
else if(attr.type == NodeVisitor.TYPE_FIRST_INT) {
if(StringUtils.equals(attr.name, ManifestPatchConstants.PATTERN_VERSION_CODE)){
} else if (attr.type == NodeVisitor.TYPE_FIRST_INT) {
if (StringUtils.equals(attr.name, ManifestPatchConstants.PATTERN_VERSION_CODE)) {
versionCode.set((int) attr.obj);
}
}
@ -207,13 +233,13 @@ public class ApkPatcher {
return true;
}
}
if(manifest.getTargetPackageName() != null && packageName.get() != null && !manifest.getTargetPackageName().contains(packageName.get())) {
if (manifest.getTargetPackageName() != null && packageName.get() != null && !manifest.getTargetPackageName().contains(packageName.get())) {
return true;
}
return false;
});
return modifyManifest;
}catch (Exception e) {
} catch (Exception e) {
errorMessage.set(e.getLocalizedMessage());
return null;
}
@ -221,12 +247,14 @@ public class ApkPatcher {
/**
* 重新签名安装包
*
* @param apkPath APK文件路径
* @return 签名后的安装包路径
*/
public String sign(String apkPath) {
try {
File externalFilesDir = Environment.getExternalStorageDirectory();
emitProgress(47);
if (externalFilesDir != null) {
String signApkPath = externalFilesDir.getAbsolutePath() + "/SMAPI Installer/base_signed.apk";
KeyStore ks = new KeyStoreFileManager.JksKeyStore();
@ -237,16 +265,36 @@ public class ApkPatcher {
X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias);
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder("debug", privateKey, Collections.singletonList(publicKey)).build();
ZipAligner.alignZip(apkPath, signApkPath);
new File(apkPath).delete();
FileUtils.moveFile(new File(signApkPath), new File(apkPath));
emitProgress(49);
ApkSigner signer = new ApkSigner.Builder(Collections.singletonList(signerConfig))
.setInputApk(new File(apkPath))
.setOutputApk(new File(signApkPath))
.setV1SigningEnabled(true)
.setV2SigningEnabled(true).build();
long zipOpElapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
stopwatch.reset();
Thread thread = new Thread(() -> {
stopwatch.start();
while (true){
try {
Thread.sleep(20);
long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
double progress = elapsed * 0.98 / zipOpElapsed;
if(progress < 1.0) {
emitProgress((int) (49 + 40 * progress));
}
} catch (InterruptedException ignored) {
return;
}
}
});
thread.start();
signer.sign();
new File(apkPath).delete();
if(thread.isAlive() && !thread.isInterrupted()) {
thread.interrupt();
}
FileUtils.forceDelete(new File(apkPath));
emitProgress(90);
return signApkPath;
}
} catch (Exception e) {
@ -258,6 +306,7 @@ public class ApkPatcher {
/**
* 对指定安装包发起安装
*
* @param apkPath 安装包路径
*/
public void install(String apkPath) {
@ -290,6 +339,7 @@ public class ApkPatcher {
/**
* 获取报错内容
*
* @return 报错内容
*/
public AtomicReference<String> getErrorMessage() {
@ -299,4 +349,14 @@ public class ApkPatcher {
public AtomicInteger getSwitchAction() {
return switchAction;
}
private void emitProgress(int progress) {
for (Consumer<Integer> consumer : progressListener) {
consumer.accept(progress);
}
}
public void registerProgressListener(Consumer<Integer> listener) {
progressListener.add(listener);
}
}

View File

@ -19,6 +19,8 @@ import com.zane.smapiinstaller.utils.DialogUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
@ -74,21 +76,24 @@ public class InstallFragment extends Fragment {
task = new Thread(() -> {
try {
ApkPatcher patcher = new ApkPatcher(context);
DialogUtils.setProgressDialogState(root, dialog, R.string.extracting_package, 0);
patcher.registerProgressListener((progress)->{
DialogUtils.setProgressDialogState(root, dialog, null, progress);
});
DialogUtils.setProgressDialogState(root, dialog, R.string.extracting_package, null);
String path = patcher.extract();
if (path == null) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found)));
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 10);
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, null);
if (!CommonLogic.unpackSmapiFiles(context, path, false)) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_unpack_smapi_files)));
return;
}
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 15);
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 6);
modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(root, dialog, R.string.patching_package, 25);
DialogUtils.setProgressDialogState(root, dialog, R.string.patching_package, 8);
if (!patcher.patch(path)) {
int target = patcher.getSwitchAction().getAndSet(0);
if(target == R.string.menu_download) {
@ -104,15 +109,14 @@ public class InstallFragment extends Fragment {
}
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.signing_package, 55);
DialogUtils.setProgressDialogState(root, dialog, R.string.signing_package, null);
String signPath = patcher.sign(path);
if (signPath == null) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.installing_package, 99);
DialogUtils.setProgressDialogState(root, dialog, R.string.installing_package, null);
patcher.install(signPath);
dialog.incrementProgress(1);
}
catch (Exception e) {
Crashes.trackError(e);

View File

@ -28,12 +28,16 @@ public class DialogUtils {
* @param message 消息
* @param progress 进度
*/
public static void setProgressDialogState(View view, MaterialDialog dialog, int message, int progress) {
public static void setProgressDialogState(View view, MaterialDialog dialog, Integer message, Integer progress) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing() && !dialog.isCancelled()) {
activity.runOnUiThread(() -> {
dialog.setProgress(progress);
dialog.setContent(message);
if(progress != null) {
dialog.setProgress(progress);
}
if(message != null) {
dialog.setContent(message);
}
});
}
}

View File

@ -0,0 +1,74 @@
package com.zane.smapiinstaller.utils;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import net.fornwall.apksigner.zipio.ZioEntry;
import net.fornwall.apksigner.zipio.ZipInput;
import net.fornwall.apksigner.zipio.ZipOutput;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java9.util.function.Consumer;
import lombok.AllArgsConstructor;
import lombok.Data;
public class ZipUtils {
public static void addOrReplaceEntries(String inputZipFilename, List<ZipEntrySource> entrySources, String outputZipFilename, Consumer<Integer> progressCallback) throws IOException {
File inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) {
throw new IllegalArgumentException("Input and output files are the same");
}
ImmutableMap<String, ZipEntrySource> entryMap = Maps.uniqueIndex(entrySources, ZipEntrySource::getPath);
try (ZipInput input = new ZipInput(inputZipFilename)) {
int size = input.entries.values().size();
int index = 0;
int reportInterval = size / 100;
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
HashSet<String> replacedFileSet = new HashSet<>(entryMap.size());
for (ZioEntry inEntry : input.entries.values()) {
if (entryMap.containsKey(inEntry.getName())) {
ZipEntrySource source = entryMap.get(inEntry.getName());
ZioEntry zioEntry = new ZioEntry(inEntry.getName());
zioEntry.setCompression(source.getCompressionMethod());
zioEntry.getOutputStream().write(source.getData());
zipOutput.write(zioEntry);
replacedFileSet.add(inEntry.getName());
} else {
zipOutput.write(inEntry);
}
index++;
if(index % reportInterval == 0) {
progressCallback.accept((int) (index * 95.0 / size));
}
}
Sets.SetView<String> difference = Sets.difference(entryMap.keySet(), replacedFileSet);
index = 0;
for (String name : difference) {
ZipEntrySource source = entryMap.get(name);
ZioEntry zioEntry = new ZioEntry(name);
zioEntry.setCompression(source.getCompressionMethod());
zioEntry.getOutputStream().write(source.getData());
zipOutput.write(zioEntry);
progressCallback.accept(95 + (int)(index * 5.0 / difference.size()));
}
progressCallback.accept(100);
}
}
}
@Data
@AllArgsConstructor
public static class ZipEntrySource {
private String path;
private byte[] data;
private int compressionMethod;
}
}

View File

@ -1,29 +0,0 @@
package net.fornwall.apksigner;
import net.fornwall.apksigner.zipio.ZioEntry;
import net.fornwall.apksigner.zipio.ZipInput;
import net.fornwall.apksigner.zipio.ZipOutput;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
public class ZipAligner {
public static void alignZip(String inputZipFilename, String outputZipFilename) throws IOException, GeneralSecurityException {
File inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) {
throw new IllegalArgumentException("Input and output files are the same");
}
try (ZipInput input = new ZipInput(inputZipFilename)) {
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
for (ZioEntry inEntry : input.entries.values()) {
zipOutput.write(inEntry);
}
}
}
}
}