diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7d4e29f..08c5de3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -138,6 +138,7 @@ -keep class pxb.android.** { *; } -keep class net.fornwall.apksigner.** { *; } -keep class com.android.apksig.** { *; } +-keep class com.fansin.message.tool.core.BufferedRandomAccessFile { *; } #Warning:org.bouncycastle.jce.provider.X509LDAPCertStoreSpi: can't find referenced class javax.naming.NamingEnumeration -dontwarn javax.naming.** @@ -162,27 +163,27 @@ public static java.lang.String TABLENAME; # If you do NOT use RxJava: -dontwarn rx.** --keep class com.sun.org.apache.xml.internal.utils.PrefixResolver --keep class java.rmi.Remote --keep class java.rmi.server.* --keep class javax.annotation.processing.AbstractProcessor --keep class javax.el.* --keep class javax.servlet.http.* --keep class javax.servlet.jsp.el.VariableResolver --keep class javax.servlet.jsp.* --keep class javax.servlet.jsp.tagext.* --keep class javax.servlet.* --keep class javax.swing.JTree --keep class javax.swing.tree.TreeNode --keep class lombok.core.configuration.ConfigurationKey --keep class org.apache.tools.ant.Task --keep class org.apache.tools.ant.taskdefs.MatchingTask --keep class org.apache.xml.utils.PrefixResolver --keep class org.jaxen.dom.* --keep class org.jaxen.dom4j.Dom4jXPath --keep class org.jaxen.jdom.JDOMXPath --keep class org.jaxen.* --keep class org.jdom.output.XMLOutputter --keep class org.python.core.PyObject --keep class org.python.util.PythonInterpreter --keep class org.zeroturnaround.javarebel.ClassEventListener +-dontwarn com.sun.org.apache.xml.internal.utils.PrefixResolver +-dontwarn java.rmi.Remote +-dontwarn java.rmi.server.* +-dontwarn javax.annotation.processing.AbstractProcessor +-dontwarn javax.el.* +-dontwarn javax.servlet.http.* +-dontwarn javax.servlet.jsp.el.VariableResolver +-dontwarn javax.servlet.jsp.* +-dontwarn javax.servlet.jsp.tagext.* +-dontwarn javax.servlet.* +-dontwarn javax.swing.JTree +-dontwarn javax.swing.tree.TreeNode +-dontwarn lombok.core.configuration.ConfigurationKey +-dontwarn org.apache.tools.ant.Task +-dontwarn org.apache.tools.ant.taskdefs.MatchingTask +-dontwarn org.apache.xml.utils.PrefixResolver +-dontwarn org.jaxen.dom.* +-dontwarn org.jaxen.dom4j.Dom4jXPath +-dontwarn org.jaxen.jdom.JDOMXPath +-dontwarn org.jaxen.* +-dontwarn org.jdom.output.XMLOutputter +-dontwarn org.python.core.PyObject +-dontwarn org.python.util.PythonInterpreter +-dontwarn org.zeroturnaround.javarebel.ClassEventListener diff --git a/app/src/main/java/com/android/apksig/ApkVerifier.java b/app/src/main/java/com/android/apksig/ApkVerifier.java index 3e1e7da..8ebe3a1 100644 --- a/app/src/main/java/com/android/apksig/ApkVerifier.java +++ b/app/src/main/java/com/android/apksig/ApkVerifier.java @@ -20,17 +20,17 @@ import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.apk.AndroidBinXmlParser; import com.android.apksig.internal.apk.ApkSigningBlockUtils; -import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.apk.v2.V2SchemeVerifier; -import com.android.apksig.internal.apk.v3.V3SchemeVerifier; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.apksig.util.RunnablesExecutor; import com.android.apksig.zip.ZipFormatException; + import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -47,6 +47,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; /** * APK signature verifier which mimics the behavior of the Android platform. @@ -211,54 +215,31 @@ public class ApkVerifier { // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature // verification. If the signature is found but does not verify, the APK is rejected. Set foundApkSigSchemeIds = new HashSet<>(2); - if (maxSdkVersion >= AndroidSdkVersion.N) { - RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; - // Android P and newer attempts to verify APKs using APK Signature Scheme v3 - if (maxSdkVersion >= AndroidSdkVersion.P) { - try { - ApkSigningBlockUtils.Result v3Result = - V3SchemeVerifier.verify( - executor, - apk, - zipSections, - Math.max(minSdkVersion, AndroidSdkVersion.P), - maxSdkVersion); - foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); - result.mergeFrom(v3Result); - } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { - // v3 signature not required - } - if (result.containsErrors()) { - return result; + foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + FutureTask taskV2 = new FutureTask<>(() -> { + if (maxSdkVersion >= AndroidSdkVersion.N) { + // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P + // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or + // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if + // no APK Signature Scheme v3 (or newer scheme) signatures were found. + if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) { + try { + RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; + return V2SchemeVerifier.verify( + executor, + apk, + zipSections, + supportedSchemeNames, + foundApkSigSchemeIds, + Math.max(minSdkVersion, AndroidSdkVersion.N), + maxSdkVersion); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v2 signature not required + } } } - - // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P - // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or - // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if - // no APK Signature Scheme v3 (or newer scheme) signatures were found. - if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) { - try { - ApkSigningBlockUtils.Result v2Result = - V2SchemeVerifier.verify( - executor, - apk, - zipSections, - supportedSchemeNames, - foundApkSigSchemeIds, - Math.max(minSdkVersion, AndroidSdkVersion.N), - maxSdkVersion); - foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); - result.mergeFrom(v2Result); - } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { - // v2 signature not required - } - if (result.containsErrors()) { - return result; - } - } - } - + return null; + }); // Android O and newer requires that APKs targeting security sandbox version 2 and higher // are signed using APK Signature Scheme v2 or newer. if (maxSdkVersion >= AndroidSdkVersion.O) { @@ -280,16 +261,27 @@ public class ApkVerifier { // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer // scheme) signatures were found. - if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { - V1SchemeVerifier.Result v1Result = - V1SchemeVerifier.verify( - apk, - zipSections, - supportedSchemeNames, - foundApkSigSchemeIds, - minSdkVersion, - maxSdkVersion); - result.mergeFrom(v1Result); + FutureTask taskV1 = new FutureTask<>(() -> { + if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { + return V1SchemeVerifier.verify( + apk, + zipSections, + supportedSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion); + } + return null; + }); + ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); + executorService.submit(taskV1); + executorService.submit(taskV2); + executorService.shutdown(); + try { + result.mergeFrom(taskV1.get()); + result.mergeFrom(taskV2.get()); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); } if (result.containsErrors()) { return result; @@ -632,6 +624,9 @@ public class ApkVerifier { } private void mergeFrom(V1SchemeVerifier.Result source) { + if(source == null) { + return; + } mVerifiedUsingV1Scheme = source.verified; mErrors.addAll(source.getErrors()); mWarnings.addAll(source.getWarnings()); @@ -644,6 +639,9 @@ public class ApkVerifier { } private void mergeFrom(ApkSigningBlockUtils.Result source) { + if(source == null) { + return; + } switch (source.signatureSchemeVersion) { case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: mVerifiedUsingV2Scheme = source.verified; diff --git a/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java index ac3d601..9437fad 100644 --- a/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java +++ b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -32,8 +32,8 @@ import com.android.apksig.internal.util.TeeDataSink; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSinks; import com.android.apksig.util.DataSource; - import com.android.apksig.util.RunnablesExecutor; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; @@ -53,7 +53,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.stream.StreamSupport; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Default implementation of {@link ApkSignerEngine}. @@ -432,21 +433,25 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { Pair> sections = V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult); String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm); - for (Map.Entry entry: sections.getSecond().entrySet()) { - String entryName = entry.getKey(); - if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) && - isDebuggable(entryName)) { - - Optional extractedDigest = - V1SchemeVerifier.getDigestsToVerify( - entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream() - .filter(d -> d.jcaDigestAlgorithm == alg) - .findFirst(); - - extractedDigest.ifPresent( - namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest)); - } + Stream> entryStream; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + entryStream = sections.getSecond().entrySet().parallelStream(); } + else { + entryStream = sections.getSecond().entrySet().stream(); + } + entryStream.filter(entry->V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) && + isDebuggable(entry.getKey()) && entryNames.contains(entry.getKey())).forEach(entry->{ + Optional extractedDigest = + V1SchemeVerifier.getDigestsToVerify( + entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream() + .filter(d -> d.jcaDigestAlgorithm.equals(alg)) + .findFirst(); + + extractedDigest.ifPresent( + namedDigest -> mOutputJarEntryDigests.put(entry.getKey(), namedDigest.digest)); + + }); return mOutputJarEntryDigests.keySet(); } diff --git a/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java b/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java index dae1519..5915e36 100644 --- a/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java +++ b/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java @@ -50,14 +50,15 @@ public class ChainedDataSource implements DataSource { } for (DataSource src : mSources) { + long srcSize = src.size(); // Offset is beyond the current source. Skip. - if (offset >= src.size()) { - offset -= src.size(); + if (offset >= srcSize) { + offset -= srcSize; continue; } // If the remaining is enough, finish it. - long remaining = src.size() - offset; + long remaining = srcSize - offset; if (remaining >= size) { src.feed(offset, size, sink); break; 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 1abadd7..a5ef7a5 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java @@ -12,6 +12,9 @@ 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; @@ -23,6 +26,7 @@ import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.constant.Constants; import com.zane.smapiinstaller.constant.DialogAction; import com.zane.smapiinstaller.constant.ManifestPatchConstants; +import com.zane.smapiinstaller.dto.Tuple2; import com.zane.smapiinstaller.entity.ApkFilesManifest; import com.zane.smapiinstaller.entity.ManifestEntry; import com.zane.smapiinstaller.utils.DialogUtils; @@ -39,6 +43,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.RandomAccessFile; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -46,6 +51,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -79,8 +85,12 @@ public class ApkPatcher { private final AtomicInteger switchAction = new AtomicInteger(); + private Tuple2> originSignInfo = null; + private final List> progressListener = new ArrayList<>(); + private int lastProgress = -1; + private final Stopwatch stopwatch = Stopwatch.createUnstarted(new Ticker() { @Override public long read() { @@ -89,6 +99,7 @@ public class ApkPatcher { }); public ApkPatcher(Context context) { + this.lastProgress = -1; this.context = context; } @@ -120,14 +131,6 @@ public class ApkPatcher { File apkFile = new File(sourceDir); String stadewValleyBasePath = FileUtils.getStadewValleyBasePath(); - File dest = new File(stadewValleyBasePath + "/SMAPI Installer/"); - if (!dest.exists()) { - if (!dest.mkdir()) { - errorMessage.set(String.format(context.getString(R.string.error_failed_to_create_file), dest.getAbsolutePath())); - return null; - } - } - File distFile = new File(dest, apkFile.getName()); if (advancedStage == 0) { AtomicInteger count = new AtomicInteger(); ZipUtil.unpack(apkFile, new File(stadewValleyBasePath + "/StardewValley/"), name -> { @@ -140,7 +143,7 @@ public class ApkPatcher { } return null; }); - return distFile.getAbsolutePath(); + return apkFile.getAbsolutePath(); } else if (advancedStage == 1) { File contentFolder = new File(stadewValleyBasePath + "/StardewValley/Content"); if (contentFolder.exists()) { @@ -151,16 +154,11 @@ public class ApkPatcher { } else { extract(0); } - ZipUtils.removeEntries(sourceDir, "assets/Content", distFile.getAbsolutePath(), (progress) -> emitProgress((int) (progress * 0.05))); - } else { - Files.copy(apkFile, distFile); + return apkFile.getAbsolutePath(); } emitProgress(5); - return distFile.getAbsolutePath(); + return apkFile.getAbsolutePath(); } catch (PackageManager.NameNotFoundException ignored) { - } catch (IOException e) { - Log.e(TAG, "Extract error", e); - errorMessage.set(e.getLocalizedMessage()); return null; } } @@ -172,10 +170,11 @@ public class ApkPatcher { * 将指定APK文件重新打包,添加SMAPI,修改AndroidManifest.xml,同时验证版本是否正确 * * @param apkPath APK文件路径 + * @param targetFile 目标文件 * @param isAdvanced 是否高级模式 * @return 是否成功打包 */ - public boolean patch(String apkPath, boolean isAdvanced) { + public boolean patch(String apkPath, File targetFile, boolean isAdvanced) { if (apkPath == null) { return false; } @@ -205,17 +204,13 @@ public class ApkPatcher { .filter(Objects::nonNull).flatMap(Stream::of).distinct().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, + originSignInfo = ZipUtils.addOrReplaceEntries(apkPath, entries, targetFile.getAbsolutePath(), + isAdvanced ? (entryName) -> entryName.startsWith("assets/Content") : null, (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) { @@ -403,21 +398,29 @@ public class ApkPatcher { String alias = ks.aliases().nextElement(); 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(); emitProgress(49); File outputFile = new File(signApkPath); - ApkSigner signer = new ApkSigner.Builder(Collections.singletonList(signerConfig)) - .setInputApk(new File(apkPath)) - .setOutputApk(outputFile) - .setV1SigningEnabled(true) - .setV2SigningEnabled(true).build(); + List engineSignerConfigs = Collections.singletonList( + new DefaultApkSignerEngine.SignerConfig.Builder( + "debug", + privateKey, + Collections.singletonList(publicKey)) + .build()); + DefaultApkSignerEngine signerEngine = new DefaultApkSignerEngine.Builder(engineSignerConfigs, 19) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(false) + .build(); + if(originSignInfo != null && originSignInfo.getFirst() != null) { + signerEngine.initWith(originSignInfo.getFirst(), originSignInfo.getSecond()); + } long zipOpElapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); stopwatch.reset(); Thread thread = new Thread(() -> { stopwatch.start(); while (true) { try { - Thread.sleep(20); + Thread.sleep(200); long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); double progress = elapsed * 0.98 / zipOpElapsed; if (progress < 1.0) { @@ -429,7 +432,13 @@ public class ApkPatcher { } }); thread.start(); - signer.sign(); + try(RandomAccessFile inputApkFile = new RandomAccessFile(apkPath, "r")) { + ApkSigner signer = new ApkSigner.Builder(signerEngine) + .setInputApk(DataSources.asDataSource(inputApkFile, 0, inputApkFile.length())) + .setOutputApk(outputFile) + .build(); + signer.sign(); + } FileUtils.forceDelete(new File(apkPath)); ApkVerifier.Result result = new ApkVerifier.Builder(outputFile).build().verify(); if (thread.isAlive() && !thread.isInterrupted()) { @@ -518,8 +527,11 @@ public class ApkPatcher { } private void emitProgress(int progress) { - for (Consumer consumer : progressListener) { - consumer.accept(progress); + if(lastProgress < progress) { + lastProgress = progress; + for (Consumer consumer : progressListener) { + consumer.accept(progress); + } } } diff --git a/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java b/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java index 34b1cfd..341b896 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java @@ -39,11 +39,13 @@ import com.zane.smapiinstaller.utils.ZipUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.zeroturnaround.zip.ZipUtil; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.channels.Channels; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -298,8 +300,8 @@ public class CommonLogic { if (entry.isXALZ()) { bytes = ZipUtils.decompressXALZ(bytes); } - try (FileOutputStream stream = FileUtils.openOutputStream(targetFile)) { - stream.write(bytes); + try (FileOutputStream outputStream = FileUtils.openOutputStream(targetFile)) { + ByteStreams.copy(Channels.newChannel(new ByteArrayInputStream(bytes)), outputStream.getChannel()); } catch (IOException ignore) { } } else { @@ -312,7 +314,7 @@ public class CommonLogic { if (entry.isExternal() && apkFilesManifest != null) { byte[] bytes = FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath()); try (FileOutputStream outputStream = new FileOutputStream(targetFile)) { - outputStream.write(bytes); + ByteStreams.copy(Channels.newChannel(new ByteArrayInputStream(bytes)), outputStream.getChannel()); } catch (IOException ignored) { } } else { @@ -358,7 +360,7 @@ public class CommonLogic { } } try (FileOutputStream outputStream = new FileOutputStream(targetFile)) { - ByteStreams.copy(inputStream, outputStream); + ByteStreams.copy(Channels.newChannel(inputStream), outputStream.getChannel()); } } catch (IOException e) { Log.e("COMMON", "Copy Error", e); 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 aff7e19..c5e5b45 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 @@ -119,7 +119,15 @@ public class InstallFragment extends Fragment { patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(binding.getRoot(), dialog, null, progress)); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.extracting_package, null); String path = patcher.extract(isAdv ? 1 : -1); - if (path == null) { + String stadewValleyBasePath = FileUtils.getStadewValleyBasePath(); + File dest = new File(stadewValleyBasePath + "/SMAPI Installer/"); + boolean failed = path == null; + if (!dest.exists()) { + if (!dest.mkdir()) { + failed = true; + } + } + if (failed) { DialogUtils.showAlertDialog(binding.getRoot(), R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found))); return; } @@ -132,7 +140,8 @@ public class InstallFragment extends Fragment { DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.unpacking_smapi_files, 6); modAssetsManager.installDefaultMods(); DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.patching_package, 8); - if (!patcher.patch(path, isAdv)) { + File targetApk = new File(dest, "base.apk"); + if (!patcher.patch(path, targetApk, isAdv)) { 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) -> { @@ -147,7 +156,7 @@ public class InstallFragment extends Fragment { return; } DialogUtils.setProgressDialogState(binding.getRoot(), dialog, R.string.signing_package, null); - String signPath = patcher.sign(path); + 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; diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/MultiprocessingUtil.java b/app/src/main/java/com/zane/smapiinstaller/utils/MultiprocessingUtil.java new file mode 100644 index 0000000..008af9e --- /dev/null +++ b/app/src/main/java/com/zane/smapiinstaller/utils/MultiprocessingUtil.java @@ -0,0 +1,88 @@ +package com.zane.smapiinstaller.utils; + +import android.os.Build; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.SneakyThrows; + +/** + * @author Zane + */ +public class MultiprocessingUtil { + public final static ExecutorService EXECUTOR_SERVICE = getExecutorService(); + + public static ExecutorService getExecutorService() { + int processors = Runtime.getRuntime().availableProcessors(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return new ForkJoinPool(processors, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); + } + return new ThreadPoolExecutor(processors, processors, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().build()); + } + + public static TaskBundle newTaskBundle(Consumer onResult) { + LinkedBlockingQueue futureResults = new LinkedBlockingQueue<>(); + AtomicLong taskCount = new AtomicLong(1); + CountDownLatch doneLatch = new CountDownLatch(1); + EXECUTOR_SERVICE.submit(() -> { + while (true) { + try { + T result = futureResults.poll(50, TimeUnit.MILLISECONDS); + if (result != null) { + onResult.accept(result); + } else { + if (taskCount.get() == 0) { + if (futureResults.isEmpty()) { + doneLatch.countDown(); + return; + } + } + } + } catch (InterruptedException ignored) { + doneLatch.countDown(); + return; + } + } + }); + return new TaskBundle<>(taskCount, doneLatch, futureResults); + } + + @Data + @AllArgsConstructor + public static class TaskBundle { + private AtomicLong taskCount; + private CountDownLatch doneLatch; + private LinkedBlockingQueue futureResults; + + public void submitTask(Supplier task) { + EXECUTOR_SERVICE.submit(() -> { + taskCount.incrementAndGet(); + try { + futureResults.add(task.get()); + } finally { + taskCount.decrementAndGet(); + } + }); + } + + @SneakyThrows + public void join() { + taskCount.decrementAndGet(); + doneLatch.await(); + } + } +} 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 b75743b..8153848 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java @@ -3,6 +3,8 @@ package com.zane.smapiinstaller.utils; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.google.common.io.ByteStreams; +import com.zane.smapiinstaller.dto.Tuple2; import net.fornwall.apksigner.zipio.ZioEntry; import net.fornwall.apksigner.zipio.ZipInput; @@ -10,7 +12,6 @@ import net.fornwall.apksigner.zipio.ZipOutput; import net.jpountz.lz4.LZ4Factory; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; -import org.bouncycastle.util.io.Streams; import java.io.ByteArrayInputStream; import java.io.File; @@ -18,9 +19,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import lombok.AllArgsConstructor; @@ -36,7 +40,7 @@ public class ZipUtils { public static byte[] decompressXALZ(byte[] bytes) { if (bytes == null) { - return bytes; + return new byte[0]; } if (FILE_HEADER_XALZ.equals(new String(ByteUtils.subArray(bytes, 0, 4), StandardCharsets.ISO_8859_1))) { byte[] length = ByteUtils.subArray(bytes, 8, 12); @@ -46,52 +50,96 @@ public class ZipUtils { return bytes; } - public static void addOrReplaceEntries(String inputZipFilename, List entrySources, String outputZipFilename, Consumer progressCallback) throws IOException { + public static Tuple2> addOrReplaceEntries(String inputZipFilename, 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)) { throw new IllegalArgumentException("Input and output files are the same"); } 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(); - int index = 0; + AtomicLong count = new AtomicLong(); int reportInterval = size / 100; try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) { - HashSet replacedFileSet = new HashSet<>(entryMap.size()); + ConcurrentHashMap replacedFileSet = new ConcurrentHashMap<>(entryMap.size()); + MultiprocessingUtil.TaskBundle taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> { + try { + zipOutput.write(zioEntry); + long index = count.incrementAndGet(); + if (index % reportInterval == 0) { + progressCallback.accept((int) (index * 95.0 / size)); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + ZioEntry manifest = input.entries.get("META-INF/MANIFEST.MF"); + if(manifest != null) { + originManifest = manifest.getData(); + } for (ZioEntry inEntry : input.entries.values()) { - if (entryMap.containsKey(inEntry.getName())) { - ZipEntrySource source = entryMap.get(inEntry.getName()); - ZioEntry zioEntry = new ZioEntry(inEntry.getName()); + if (removePredict != null && removePredict.apply(inEntry.getName())) { + continue; + } + taskBundle.submitTask(()->{ + if (entryMap.containsKey(inEntry.getName())) { + ZipEntrySource source = entryMap.get(inEntry.getName()); + ZioEntry zioEntry = new ZioEntry(inEntry.getName()); + zioEntry.setCompression(source.getCompressionMethod()); + try (InputStream inputStream = source.getDataStream()) { + if (inputStream != null) { + ByteStreams.copy(inputStream, zioEntry.getOutputStream()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + replacedFileSet.put(inEntry.getName(), true); + return zioEntry; + } else { + originEntryName.put(inEntry.getName(), true); + return inEntry; + } + }); + } + taskBundle.join(); + Sets.SetView difference = Sets.difference(entryMap.keySet(), replacedFileSet.keySet()); + count.set(0); + taskBundle = MultiprocessingUtil.newTaskBundle((zioEntry) -> { + try { + zipOutput.write(zioEntry); + long index = count.incrementAndGet(); + progressCallback.accept(95 + (int) (index * 5.0 / difference.size())); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + for (String name : difference) { + taskBundle.submitTask(()-> { + ZipEntrySource source = entryMap.get(name); + ZioEntry zioEntry = new ZioEntry(name); zioEntry.setCompression(source.getCompressionMethod()); try (InputStream inputStream = source.getDataStream()) { - Streams.pipeAll(inputStream, zioEntry.getOutputStream()); + ByteStreams.copy(inputStream, zioEntry.getOutputStream()); + } catch (IOException e) { + throw new RuntimeException(e); } - 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()); - try (InputStream inputStream = source.getDataStream()) { - Streams.pipeAll(inputStream, zioEntry.getOutputStream()); - } - zipOutput.write(zioEntry); - progressCallback.accept(95 + (int) (index * 5.0 / difference.size())); + return zioEntry; + }); } + taskBundle.join(); progressCallback.accept(100); } } + catch (RuntimeException e) { + if(e.getCause() != null && e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } + throw e; + } + return new Tuple2<>(originManifest, originEntryName.keySet()); } public static void removeEntries(String inputZipFilename, String prefix, String outputZipFilename, Consumer progressCallback) throws IOException { @@ -134,11 +182,8 @@ public class ZipUtils { } private InputStream getDataStream() { - // Optimize: read only once if (dataSupplier != null) { - InputStream bytes = dataSupplier.get(); - dataSupplier = null; - return bytes; + return dataSupplier.get(); } return null; } diff --git a/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntry.java b/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntry.java index c45c72c..217a1c8 100644 --- a/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntry.java +++ b/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntry.java @@ -15,8 +15,10 @@ */ package net.fornwall.apksigner.zipio; +import com.google.common.io.ByteSource; +import com.google.common.io.FileBackedOutputStream; + import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -50,6 +52,7 @@ public final class ZioEntry implements Cloneable { private int localHeaderOffset; private long dataPosition = -1; private byte[] data = null; + private ByteSource dataSource = null; private ZioEntryOutputStream entryOut = null; private static byte[] alignBytes = new byte[4]; @@ -207,8 +210,9 @@ public final class ZioEntry implements Cloneable { if (entryOut != null) { entryOut.close(); size = entryOut.getSize(); - data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray(); - compressedSize = data.length; +// data = ((FileBackedOutputStream) entryOut.wrapped).toByteArray(); + dataSource = ((FileBackedOutputStream) entryOut.wrapped).asByteSource(); + compressedSize = (int) dataSource.size(); crc32 = entryOut.getCRC(); } @@ -252,6 +256,10 @@ public final class ZioEntry implements Cloneable { if (data != null) { output.writeBytes(data); + } else if(dataSource != null){ + try (InputStream inputStream = dataSource.openStream()){ + output.pipeStream(inputStream); + } } else { zipInput.seek(dataPosition); @@ -282,17 +290,18 @@ public final class ZioEntry implements Cloneable { byte[] tmpdata = new byte[size]; - InputStream din = getInputStream(); - int count = 0; + try(InputStream din = getInputStream()) { + int count = 0; - while (count != size) { - int numRead = din.read(tmpdata, count, size - count); - if (numRead < 0) - throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size, - count)); - count += numRead; + while (count != size) { + int numRead = din.read(tmpdata, count, size - count); + if (numRead < 0) + throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size, + count)); + count += numRead; + } + return tmpdata; } - return tmpdata; } // Returns an input stream for reading the entry's data. @@ -300,11 +309,12 @@ public final class ZioEntry implements Cloneable { if (entryOut != null) { entryOut.close(); size = entryOut.getSize(); - data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray(); +// data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray(); + dataSource = ((FileBackedOutputStream) entryOut.wrapped).asByteSource(); compressedSize = data.length; crc32 = entryOut.getCRC(); entryOut = null; - InputStream rawis = new ByteArrayInputStream(data); + InputStream rawis = dataSource.openStream(); if (compression == 0) return rawis; else { @@ -332,7 +342,8 @@ public final class ZioEntry implements Cloneable { // Returns an output stream for writing an entry's data. public OutputStream getOutputStream() { - entryOut = new ZioEntryOutputStream(compression, new ByteArrayOutputStream()); + // use FileBackedOutputStream to reduce memory consumption + entryOut = new ZioEntryOutputStream(compression, new FileBackedOutputStream(128 * 1024, true)); return entryOut; } diff --git a/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntryOutputStream.java b/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntryOutputStream.java index 9024bbf..b635780 100644 --- a/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntryOutputStream.java +++ b/app/src/main/java/net/fornwall/apksigner/zipio/ZioEntryOutputStream.java @@ -32,7 +32,7 @@ final class ZioEntryOutputStream extends OutputStream { public ZioEntryOutputStream(int compression, OutputStream wrapped) { this.wrapped = wrapped; downstream = (compression == 0) ? wrapped : new DeflaterOutputStream(wrapped, new Deflater( - Deflater.BEST_COMPRESSION, true)); + compression, true)); } @Override diff --git a/app/src/main/java/net/fornwall/apksigner/zipio/ZipInput.java b/app/src/main/java/net/fornwall/apksigner/zipio/ZipInput.java index 28429d8..9ee02c7 100644 --- a/app/src/main/java/net/fornwall/apksigner/zipio/ZipInput.java +++ b/app/src/main/java/net/fornwall/apksigner/zipio/ZipInput.java @@ -1,6 +1,7 @@ package net.fornwall.apksigner.zipio; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.util.LinkedHashMap; import java.util.Map; @@ -34,8 +35,11 @@ public final class ZipInput implements AutoCloseable { public Manifest getManifest() throws IOException { if (manifest == null) { ZioEntry e = entries.get("META-INF/MANIFEST.MF"); - if (e != null) - manifest = new Manifest(e.getInputStream()); + if (e != null) { + try(InputStream inputStream = e.getInputStream()) { + manifest = new Manifest(inputStream); + } + } } return manifest; } diff --git a/app/src/main/java/net/fornwall/apksigner/zipio/ZipOutput.java b/app/src/main/java/net/fornwall/apksigner/zipio/ZipOutput.java index a643cb1..7e8f503 100644 --- a/app/src/main/java/net/fornwall/apksigner/zipio/ZipOutput.java +++ b/app/src/main/java/net/fornwall/apksigner/zipio/ZipOutput.java @@ -15,7 +15,10 @@ */ package net.fornwall.apksigner.zipio; +import com.google.common.io.ByteStreams; + import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.HashSet; import java.util.LinkedList; @@ -79,6 +82,11 @@ public final class ZipOutput implements AutoCloseable { filePointer += value.length; } + public void pipeStream(InputStream inputStream) throws IOException { + long length = ByteStreams.copy(inputStream, out); + filePointer += length; + } + public void writeBytes(byte[] value, int offset, int length) throws IOException { out.write(value, offset, length); filePointer += length;