From 27d7ba816a155e3e841852ec940d19f492d55253 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Wed, 25 Mar 2020 21:26:54 +0800 Subject: [PATCH] 1.Improve patch processing speed 2.Make process bar more accurately --- app/build.gradle | 14 +-- .../entity/ModManifestEntry.java | 4 +- .../zane/smapiinstaller/logic/ApkPatcher.java | 116 +++++++++++++----- .../ui/install/InstallFragment.java | 18 +-- .../smapiinstaller/utils/DialogUtils.java | 10 +- .../zane/smapiinstaller/utils/ZipUtils.java | 74 +++++++++++ .../net/fornwall/apksigner/ZipAligner.java | 29 ----- 7 files changed, 190 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java delete mode 100644 app/src/main/java/net/fornwall/apksigner/ZipAligner.java diff --git a/app/build.gradle b/app/build.gradle index 0c7da3e..8da32df 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java b/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java index 11d27ed..2a2a823 100644 --- a/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java +++ b/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java @@ -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 { /** * 存放位置 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 4c7790f..cd4feaf 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java @@ -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> 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 packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference>() { }); + List packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference>() { + }); 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 zipEntrySourceList = new ArrayList<>(); byte[] manifest = ZipUtil.unpackEntry(file, "AndroidManifest.xml"); + emitProgress(9); List 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 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 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 getErrorMessage() { @@ -299,4 +349,14 @@ public class ApkPatcher { public AtomicInteger getSwitchAction() { return switchAction; } + + private void emitProgress(int progress) { + for (Consumer consumer : progressListener) { + consumer.accept(progress); + } + } + + public void registerProgressListener(Consumer listener) { + progressListener.add(listener); + } } 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 5de2590..79c7ae0 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 @@ -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); diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/DialogUtils.java b/app/src/main/java/com/zane/smapiinstaller/utils/DialogUtils.java index 741cfa0..fdf022b 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/DialogUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/DialogUtils.java @@ -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); + } }); } } diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java b/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java new file mode 100644 index 0000000..f7b01b3 --- /dev/null +++ b/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java @@ -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 entrySources, String outputZipFilename, Consumer 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 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 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 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; + } +} diff --git a/app/src/main/java/net/fornwall/apksigner/ZipAligner.java b/app/src/main/java/net/fornwall/apksigner/ZipAligner.java deleted file mode 100644 index 62892f2..0000000 --- a/app/src/main/java/net/fornwall/apksigner/ZipAligner.java +++ /dev/null @@ -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); - } - } - } - } - -} \ No newline at end of file