1. Optimize patch speed
This commit is contained in:
parent
3763f7757f
commit
b9cc00adbc
|
@ -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
|
||||
|
|
|
@ -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,36 +215,17 @@ 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<Integer> foundApkSigSchemeIds = new HashSet<>(2);
|
||||
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||
FutureTask<ApkSigningBlockUtils.Result> taskV2 = new FutureTask<>(() -> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
|
||||
return V2SchemeVerifier.verify(
|
||||
executor,
|
||||
apk,
|
||||
zipSections,
|
||||
|
@ -248,17 +233,13 @@ public class ApkVerifier {
|
|||
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.
|
||||
FutureTask<V1SchemeVerifier.Result> taskV1 = new FutureTask<>(() -> {
|
||||
if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
|
||||
V1SchemeVerifier.Result v1Result =
|
||||
V1SchemeVerifier.verify(
|
||||
return V1SchemeVerifier.verify(
|
||||
apk,
|
||||
zipSections,
|
||||
supportedSchemeNames,
|
||||
foundApkSigSchemeIds,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
result.mergeFrom(v1Result);
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -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<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
|
||||
V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
|
||||
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
|
||||
for (Map.Entry<String, ManifestParser.Section> entry: sections.getSecond().entrySet()) {
|
||||
String entryName = entry.getKey();
|
||||
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) &&
|
||||
isDebuggable(entryName)) {
|
||||
|
||||
Stream<Map.Entry<String, ManifestParser.Section>> 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<V1SchemeVerifier.NamedDigest> extractedDigest =
|
||||
V1SchemeVerifier.getDigestsToVerify(
|
||||
entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE).stream()
|
||||
.filter(d -> d.jcaDigestAlgorithm == alg)
|
||||
.filter(d -> d.jcaDigestAlgorithm.equals(alg))
|
||||
.findFirst();
|
||||
|
||||
extractedDigest.ifPresent(
|
||||
namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest));
|
||||
}
|
||||
}
|
||||
namedDigest -> mOutputJarEntryDigests.put(entry.getKey(), namedDigest.digest));
|
||||
|
||||
});
|
||||
return mOutputJarEntryDigests.keySet();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<byte[], Set<String>> originSignInfo = null;
|
||||
|
||||
private final List<Consumer<Integer>> 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)
|
||||
List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs = Collections.singletonList(
|
||||
new DefaultApkSignerEngine.SignerConfig.Builder(
|
||||
"debug",
|
||||
privateKey,
|
||||
Collections.singletonList(publicKey))
|
||||
.build());
|
||||
DefaultApkSignerEngine signerEngine = new DefaultApkSignerEngine.Builder(engineSignerConfigs, 19)
|
||||
.setV1SigningEnabled(true)
|
||||
.setV2SigningEnabled(true).build();
|
||||
.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();
|
||||
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,10 +527,13 @@ public class ApkPatcher {
|
|||
}
|
||||
|
||||
private void emitProgress(int progress) {
|
||||
if(lastProgress < progress) {
|
||||
lastProgress = progress;
|
||||
for (Consumer<Integer> consumer : progressListener) {
|
||||
consumer.accept(progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerProgressListener(Consumer<Integer> listener) {
|
||||
progressListener.add(listener);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <T> TaskBundle<T> newTaskBundle(Consumer<T> onResult) {
|
||||
LinkedBlockingQueue<T> 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<T> {
|
||||
private AtomicLong taskCount;
|
||||
private CountDownLatch doneLatch;
|
||||
private LinkedBlockingQueue<T> futureResults;
|
||||
|
||||
public void submitTask(Supplier<T> task) {
|
||||
EXECUTOR_SERVICE.submit(() -> {
|
||||
taskCount.incrementAndGet();
|
||||
try {
|
||||
futureResults.add(task.get());
|
||||
} finally {
|
||||
taskCount.decrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void join() {
|
||||
taskCount.decrementAndGet();
|
||||
doneLatch.await();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ZipEntrySource> entrySources, String outputZipFilename, Consumer<Integer> progressCallback) throws IOException {
|
||||
public static Tuple2<byte[], Set<String>> addOrReplaceEntries(String inputZipFilename, List<ZipEntrySource> entrySources, String outputZipFilename, Function<String, Boolean> removePredict, 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);
|
||||
byte[] originManifest = null;
|
||||
ConcurrentHashMap<String, Boolean> 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<String> replacedFileSet = new HashSet<>(entryMap.size());
|
||||
ConcurrentHashMap<String, Boolean> replacedFileSet = new ConcurrentHashMap<>(entryMap.size());
|
||||
MultiprocessingUtil.TaskBundle<ZioEntry> 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 (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()) {
|
||||
Streams.pipeAll(inputStream, zioEntry.getOutputStream());
|
||||
if (inputStream != null) {
|
||||
ByteStreams.copy(inputStream, zioEntry.getOutputStream());
|
||||
}
|
||||
zipOutput.write(zioEntry);
|
||||
replacedFileSet.add(inEntry.getName());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
replacedFileSet.put(inEntry.getName(), true);
|
||||
return zioEntry;
|
||||
} else {
|
||||
zipOutput.write(inEntry);
|
||||
originEntryName.put(inEntry.getName(), true);
|
||||
return inEntry;
|
||||
}
|
||||
index++;
|
||||
if (index % reportInterval == 0) {
|
||||
progressCallback.accept((int) (index * 95.0 / size));
|
||||
});
|
||||
}
|
||||
taskBundle.join();
|
||||
Sets.SetView<String> 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);
|
||||
}
|
||||
Sets.SetView<String> difference = Sets.difference(entryMap.keySet(), replacedFileSet);
|
||||
index = 0;
|
||||
});
|
||||
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);
|
||||
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<Integer> 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;
|
||||
}
|
||||
|
|
|
@ -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,7 +290,7 @@ public final class ZioEntry implements Cloneable {
|
|||
|
||||
byte[] tmpdata = new byte[size];
|
||||
|
||||
InputStream din = getInputStream();
|
||||
try(InputStream din = getInputStream()) {
|
||||
int count = 0;
|
||||
|
||||
while (count != size) {
|
||||
|
@ -294,17 +302,19 @@ public final class ZioEntry implements Cloneable {
|
|||
}
|
||||
return tmpdata;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an input stream for reading the entry's data.
|
||||
public InputStream getInputStream() throws IOException {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue