diff --git a/app/build.gradle b/app/build.gradle index 0ffe5f8..1e45e6d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.zane.smapiinstaller" minSdkVersion 19 targetSdkVersion 28 - versionCode 39 - versionName "1.4.8" + versionCode 40 + versionName "1.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true @@ -26,8 +26,8 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { - minifyEnabled true - shrinkResources true + minifyEnabled false + shrinkResources false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -103,4 +103,6 @@ dependencies { compileOnly 'org.projectlombok:lombok:1.18.12' annotationProcessor 'org.projectlombok:lombok:1.18.12' + + api 'com.smart.library.util:bspatch:0.0.2' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9cacbf8..fc67026 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -135,7 +135,8 @@ -dontwarn okio.** -keep class okio.**{*;} --keep class com.zane.** { *; } +-keep class com.zane.smapiinstaller.entity.** { *; } +-keep class com.zane.smapiinstaller.dto.** { *; } -keep class pxb.android.** { *; } -keep class net.fornwall.apksigner.** { *; } -keep class com.android.apksig.** { *; } diff --git a/app/src/main/assets/apk/MonoGame.Framework.dll.patch b/app/src/main/assets/apk/MonoGame.Framework.dll.patch new file mode 100644 index 0000000..4740a6b Binary files /dev/null and b/app/src/main/assets/apk/MonoGame.Framework.dll.patch differ diff --git a/app/src/main/assets/apk/classes.dex b/app/src/main/assets/apk/classes.dex deleted file mode 100644 index f6b4037..0000000 Binary files a/app/src/main/assets/apk/classes.dex and /dev/null differ diff --git a/app/src/main/assets/apk/classes.dex.patch b/app/src/main/assets/apk/classes.dex.patch new file mode 100644 index 0000000..0a57479 Binary files /dev/null and b/app/src/main/assets/apk/classes.dex.patch differ diff --git a/app/src/main/assets/apk_files_manifest.json b/app/src/main/assets/apk_files_manifest.json index 16e5356..1de3330 100644 --- a/app/src/main/assets/apk_files_manifest.json +++ b/app/src/main/assets/apk_files_manifest.json @@ -10,9 +10,10 @@ "manifestEntries": [ { "targetPath": "classes.dex", - "assetPath": "apk/classes.dex", + "assetPath": "apk/classes.dex.patch", "compression": 8, - "external": false + "external": false, + "patchCrc": "e918baf8" }, { "targetPath": "res/mipmap-mdpi-v4/ic_launcher_foreground.png", @@ -79,6 +80,14 @@ "assetPath": "apk/System.Numerics.dll", "compression": 0, "external": false + }, + { + "targetPath": "assemblies/MonoGame.Framework.dll", + "assetPath": "apk/MonoGame.Framework.dll.patch", + "compression": 0, + "external": false, + "patchCrc": "c7fec998", + "advanced": true } ] } \ No newline at end of file diff --git a/app/src/main/assets/downloadable_content_list.json b/app/src/main/assets/downloadable_content_list.json index 541a76d..b714b31 100644 --- a/app/src/main/assets/downloadable_content_list.json +++ b/app/src/main/assets/downloadable_content_list.json @@ -1,5 +1,5 @@ { - "version": 17, + "version": 18, "contents": [ { "type": "COMPAT", @@ -14,16 +14,16 @@ "name": "SMAPI for Galaxy Store", "assetPath": "compat/samsung_138/", "description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.5.0", - "url": "http://zaneyork.cn/download/compat/smapi_samsung_138_11.zip", - "hash": "4dce4537952d868d40ec18dd0f0b312259cd581f6a2ffdaa5a5e3519148f63d9" + "url": "http://zaneyork.cn/download/compat/smapi_samsung_138_12.zip", + "hash": "5e7d9c52fa5decbc7a319fce8a7f1730aa875867cdeaa339a7c95f22a96d0185" }, { "type": "COMPAT", "name": "SMAPI for Amazon Store", "assetPath": "compat/amazon_138/", "description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.5.0", - "url": "http://zaneyork.cn/download/compat/smapi_amazon_138_1.zip", - "hash": "8feac89c4b722a38408d40503cc93f8e425e06cdd27b564c03af6aeb07a384e6" + "url": "http://zaneyork.cn/download/compat/smapi_amazon_138_2.zip", + "hash": "919f4c900dcc2792b25428bc40bebb364cab6fff209d02a2ac03afc265589be8" }, { "type": "LOCALE", diff --git a/app/src/main/assets/downloadable_content_list.json.en b/app/src/main/assets/downloadable_content_list.json.en index 541a76d..b714b31 100644 --- a/app/src/main/assets/downloadable_content_list.json.en +++ b/app/src/main/assets/downloadable_content_list.json.en @@ -1,5 +1,5 @@ { - "version": 17, + "version": 18, "contents": [ { "type": "COMPAT", @@ -14,16 +14,16 @@ "name": "SMAPI for Galaxy Store", "assetPath": "compat/samsung_138/", "description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.5.0", - "url": "http://zaneyork.cn/download/compat/smapi_samsung_138_11.zip", - "hash": "4dce4537952d868d40ec18dd0f0b312259cd581f6a2ffdaa5a5e3519148f63d9" + "url": "http://zaneyork.cn/download/compat/smapi_samsung_138_12.zip", + "hash": "5e7d9c52fa5decbc7a319fce8a7f1730aa875867cdeaa339a7c95f22a96d0185" }, { "type": "COMPAT", "name": "SMAPI for Amazon Store", "assetPath": "compat/amazon_138/", "description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.5.0", - "url": "http://zaneyork.cn/download/compat/smapi_amazon_138_1.zip", - "hash": "8feac89c4b722a38408d40503cc93f8e425e06cdd27b564c03af6aeb07a384e6" + "url": "http://zaneyork.cn/download/compat/smapi_amazon_138_2.zip", + "hash": "919f4c900dcc2792b25428bc40bebb364cab6fff209d02a2ac03afc265589be8" }, { "type": "LOCALE", diff --git a/app/src/main/assets/downloadable_content_list.json.zh b/app/src/main/assets/downloadable_content_list.json.zh index c1d9cc3..daec31a 100644 --- a/app/src/main/assets/downloadable_content_list.json.zh +++ b/app/src/main/assets/downloadable_content_list.json.zh @@ -1,5 +1,5 @@ { - "version": 17, + "version": 18, "contents": [ { "type": "COMPAT", @@ -14,16 +14,16 @@ "name": "SMAPI三星商店兼容包", "assetPath": "compat/samsung_138/", "description": "SMAPI三星商店兼容包, 适用版本1.4.4.138至今, SMAPI 3.5.0", - "url": "http://zaneyork.cn/download/compat/smapi_samsung_138_11.zip", - "hash": "4dce4537952d868d40ec18dd0f0b312259cd581f6a2ffdaa5a5e3519148f63d9" + "url": "http://zaneyork.cn/download/compat/smapi_samsung_138_12.zip", + "hash": "5e7d9c52fa5decbc7a319fce8a7f1730aa875867cdeaa339a7c95f22a96d0185" }, { "type": "COMPAT", "name": "SMAPI亚马逊商店兼容包", "assetPath": "compat/amazon_138/", "description": "SMAPI亚马逊商店兼容包, 适用版本1.4.4.138至今, SMAPI 3.5.0", - "url": "http://zaneyork.cn/download/compat/smapi_amazon_138_1.zip", - "hash": "8feac89c4b722a38408d40503cc93f8e425e06cdd27b564c03af6aeb07a384e6" + "url": "http://zaneyork.cn/download/compat/smapi_amazon_138_2.zip", + "hash": "919f4c900dcc2792b25428bc40bebb364cab6fff209d02a2ac03afc265589be8" }, { "type": "LOCALE", diff --git a/app/src/main/assets/smapi/MonoMod.RuntimeDetour.dll b/app/src/main/assets/smapi/MonoMod.RuntimeDetour.dll index e87dea8..5a3ea99 100644 Binary files a/app/src/main/assets/smapi/MonoMod.RuntimeDetour.dll and b/app/src/main/assets/smapi/MonoMod.RuntimeDetour.dll differ diff --git a/app/src/main/assets/smapi/MonoMod.Utils.dll b/app/src/main/assets/smapi/MonoMod.Utils.dll index c2ec4f1..bbadff8 100644 Binary files a/app/src/main/assets/smapi/MonoMod.Utils.dll and b/app/src/main/assets/smapi/MonoMod.Utils.dll differ diff --git a/app/src/main/assets/smapi/config.json b/app/src/main/assets/smapi/config.json index 5a136fc..d30425c 100644 --- a/app/src/main/assets/smapi/config.json +++ b/app/src/main/assets/smapi/config.json @@ -27,11 +27,6 @@ The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to */ "DeveloperMode": false, - /** - * Whether to enable load mods with multithreading supports. - */ - "MultithreadingLoading": false, - /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as diff --git a/app/src/main/java/com/zane/smapiinstaller/MainActivity.java b/app/src/main/java/com/zane/smapiinstaller/MainActivity.java index c469efa..5855b8a 100644 --- a/app/src/main/java/com/zane/smapiinstaller/MainActivity.java +++ b/app/src/main/java/com/zane/smapiinstaller/MainActivity.java @@ -194,6 +194,7 @@ public class MainActivity extends AppCompatActivity { } menu.findItem(R.id.settings_developer_mode).setChecked(config.isDeveloperMode()); menu.findItem(R.id.settings_disable_mono_mod).setChecked(config.isDisableMonoMod()); + menu.findItem(R.id.settings_advanced_mode).setChecked(Boolean.parseBoolean(ConfigUtils.getConfig((MainApplication) getApplication(), AppConfigKey.ADVANCED_MODE, "false").getValue())); Constants.MOD_PATH = config.getModsPath(); return super.onPrepareOptionsMenu(menu); } @@ -262,6 +263,14 @@ public class MainActivity extends AppCompatActivity { case R.id.toolbar_update_check: checkModUpdateLogic(); return true; + case R.id.settings_advanced_mode: + AppConfig appConfig = ConfigUtils.getConfig((MainApplication) getApplication(), AppConfigKey.ADVANCED_MODE, "false"); + appConfig.setValue(String.valueOf(item.isChecked())); + ConfigUtils.saveConfig((MainApplication) getApplication(), appConfig); + startActivity(new Intent(this, MainActivity.class)); + overridePendingTransition(R.anim.fragment_fade_enter, R.anim.fragment_fade_exit); + finish(); + break; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/com/zane/smapiinstaller/constant/AppConfigKey.java b/app/src/main/java/com/zane/smapiinstaller/constant/AppConfigKey.java index f5f978c..30fa443 100644 --- a/app/src/main/java/com/zane/smapiinstaller/constant/AppConfigKey.java +++ b/app/src/main/java/com/zane/smapiinstaller/constant/AppConfigKey.java @@ -11,4 +11,6 @@ public class AppConfigKey { public static final String IGNORE_UPDATE_VERSION_CODE = "UpdateIgnoreVersionCode"; public static final String PRIVACY_POLICY_CONFIRM = "PrivacyPolicyConfirm"; + + public static final String ADVANCED_MODE = "AdvancedMode"; } diff --git a/app/src/main/java/com/zane/smapiinstaller/dto/AppUpdateCheckResultDto.java b/app/src/main/java/com/zane/smapiinstaller/dto/AppUpdateCheckResultDto.java index 7754e23..b4c2a0b 100644 --- a/app/src/main/java/com/zane/smapiinstaller/dto/AppUpdateCheckResultDto.java +++ b/app/src/main/java/com/zane/smapiinstaller/dto/AppUpdateCheckResultDto.java @@ -2,6 +2,9 @@ package com.zane.smapiinstaller.dto; import lombok.Data; +/** + * @author Zane + */ @Data public class AppUpdateCheckResultDto { /** diff --git a/app/src/main/java/com/zane/smapiinstaller/entity/ManifestEntry.java b/app/src/main/java/com/zane/smapiinstaller/entity/ManifestEntry.java index 7aaf47f..8876cc5 100644 --- a/app/src/main/java/com/zane/smapiinstaller/entity/ManifestEntry.java +++ b/app/src/main/java/com/zane/smapiinstaller/entity/ManifestEntry.java @@ -28,4 +28,14 @@ public class ManifestEntry { * 文件是否不属于兼容包中 */ private boolean external; + + /** + * 补丁CRC + */ + private String patchCrc; + + /** + * 是否为高级模式补丁 + */ + private boolean advanced; } 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 ce7a437..9364cee 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java @@ -16,6 +16,7 @@ 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.hash.Hashing; import com.google.common.io.Files; import com.zane.smapiinstaller.BuildConfig; import com.zane.smapiinstaller.R; @@ -44,13 +45,12 @@ 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.Objects; +import java9.util.function.Consumer; +import java9.util.stream.Collectors; import java9.util.stream.StreamSupport; import pxb.android.axml.NodeVisitor; @@ -83,6 +83,10 @@ public class ApkPatcher { * @return 抽取后的APK文件路径,如果抽取失败返回null */ public String extract() { + return extract(-1); + } + + public String extract(int advancedStage) { emitProgress(0); PackageManager packageManager = context.getPackageManager(); List packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference>() { @@ -108,12 +112,31 @@ public class ApkPatcher { } } File distFile = new File(dest, apkFile.getName()); - Files.copy(apkFile, distFile); + if (advancedStage == 0) { + AtomicInteger count = new AtomicInteger(); + ZipUtil.unpack(apkFile, new File(externalFilesDir.getAbsolutePath() + "/StardewValley/"), name -> { + if (name.startsWith("assets/")) { + int progress = count.incrementAndGet(); + if (progress % 30 == 0) { + emitProgress(progress / 30); + } + return name.replaceFirst("assets/", ""); + } + return null; + }); + } else if (advancedStage == 1) { + ZipUtils.removeEntries(sourceDir, "assets/Content", distFile.getAbsolutePath(), (progress) -> emitProgress((int) (progress * 0.05))); + } else { + Files.copy(apkFile, distFile); + } emitProgress(5); return distFile.getAbsolutePath(); } - } catch (PackageManager.NameNotFoundException | IOException e) { + } catch (PackageManager.NameNotFoundException ignored) { + } catch (IOException e) { Log.e(TAG, "Extract error", e); + errorMessage.set(e.getLocalizedMessage()); + return null; } } errorMessage.set(context.getString(R.string.error_game_not_found)); @@ -127,6 +150,10 @@ public class ApkPatcher { * @return 是否成功打包 */ public boolean patch(String apkPath) { + return patch(apkPath, false); + } + + public boolean patch(String apkPath, boolean advanced) { if (apkPath == null) { return false; } @@ -150,13 +177,37 @@ public class ApkPatcher { } ApkFilesManifest apkFilesManifest = apkFilesManifests.get(0); List manifestEntries = apkFilesManifest.getManifestEntries(); + errorMessage.set(null); 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()); + if(entry.isAdvanced() && !advanced) { + return null; } - }).collect(Collectors.toList()); + byte[] bytes; + if (entry.isExternal()) { + bytes = FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath()); + } else { + bytes = FileUtils.getAssetBytes(context, entry.getAssetPath()); + } + if (StringUtils.isNoneBlank(entry.getPatchCrc())) { + byte[] originBytes = ZipUtil.unpackEntry(file, entry.getTargetPath()); + if (originBytes != null) { + String crc = Integer.toHexString(Hashing.crc32().hashBytes(originBytes).hashCode()); + if (StringUtils.equals(crc, entry.getPatchCrc())) { + bytes = FileUtils.patchFile(originBytes, bytes); + if (bytes == null) { + errorMessage.set("Patch failed"); + } + } + else { + return null; + } + } + } + return new ZipUtils.ZipEntrySource(entry.getTargetPath(), bytes, entry.getCompression()); + }).filter(Objects::nonNull).collect(Collectors.toList()); + if (errorMessage.get() != null) { + return false; + } entries.add(new ZipUtils.ZipEntrySource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED)); emitProgress(10); String patchedFilename = apkPath + ".patched"; @@ -191,7 +242,7 @@ public class ApkPatcher { AtomicReference versionName = new AtomicReference<>(); AtomicLong versionCode = new AtomicLong(); Predicate processLogic = (attr) -> { - if(attr == null) { + if (attr == null) { return true; } if (attr.type == NodeVisitor.TYPE_STRING) { @@ -236,11 +287,11 @@ public class ApkPatcher { }; try { byte[] modifyManifest = CommonLogic.modifyManifest(bytes, processLogic); - if(StringUtils.endsWith(versionName.get(), ManifestPatchConstants.PATTERN_VERSION_AMAZON)) { + if (StringUtils.endsWith(versionName.get(), ManifestPatchConstants.PATTERN_VERSION_AMAZON)) { packageName.set(ManifestPatchConstants.APP_PACKAGE_NAME + ManifestPatchConstants.PATTERN_VERSION_AMAZON); } Iterables.removeIf(manifests, manifest -> { - if(manifest == null) { + if (manifest == null) { return true; } if (versionCode.get() < manifest.getMinBuildCode()) { @@ -314,7 +365,7 @@ public class ApkPatcher { if (thread.isAlive() && !thread.isInterrupted()) { thread.interrupt(); } - if(result.containsErrors()) { + if (result.containsErrors()) { errorMessage.set(StreamSupport.stream(result.getErrors()).map(ApkVerifier.IssueWithParams::toString).collect(Collectors.joining(","))); return null; } 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 8bf6607..9969d06 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java @@ -23,6 +23,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; +import com.lmntrx.android.library.livin.missme.ProgressDialog; +import com.microsoft.appcenter.crashes.Crashes; import com.zane.smapiinstaller.MainApplication; import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.constant.DialogAction; @@ -40,6 +42,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java9.util.function.BiConsumer; import java9.util.function.Consumer; @@ -305,12 +308,12 @@ public class CommonLogic { CommonLogic.doOnNonNull(activity, (context) -> { try { Intent intent = new Intent("android.intent.action.VIEW"); - intent.setData(Uri.parse("market://details?id=" + context.getPackageName())); + intent.setData(Uri.parse("market://details?id=com.zane.smapiinstaller")); intent.setPackage("com.android.vending"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } catch (Exception ex) { - CommonLogic.openUrl(activity, "https://play.google.com/store/apps/details?id=" + context.getPackageName()); + CommonLogic.openUrl(activity, "https://play.google.com/store/apps/details?id=com.zane.smapiinstaller"); } }); } @@ -320,4 +323,30 @@ public class CommonLogic { String policy = FileUtils.getLocaledAssetText(context, "privacy_policy.txt"); DialogUtils.showConfirmDialog(view, R.string.privacy_policy, policy, R.string.confirm, R.string.cancel, true, callback); } + + public static void showProgressDialog(View root, Context context, Consumer dialogConsumer) { + AtomicReference dialogHolder = DialogUtils.showProgressDialog(root, R.string.install_progress_title, context.getString(R.string.extracting_package)); + ProgressDialog dialog = null; + try { + do { + Thread.sleep(10); + dialog = dialogHolder.get(); + } while (dialog == null); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + ConfigManager configManager = new ConfigManager(); + if (configManager.getConfig().isInitial()) { + configManager.getConfig().setInitial(false); + configManager.getConfig().setDisableMonoMod(true); + configManager.flushConfig(); + } + } + dialogConsumer.accept(dialog); + } catch (InterruptedException ignored) { + } catch (Exception e) { + Crashes.trackError(e); + DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage()); + } finally { + DialogUtils.dismissDialog(root, dialog); + } + } } diff --git a/app/src/main/java/com/zane/smapiinstaller/logic/ModAssetsManager.java b/app/src/main/java/com/zane/smapiinstaller/logic/ModAssetsManager.java index c4c2984..b02724b 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ModAssetsManager.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ModAssetsManager.java @@ -47,6 +47,7 @@ import java9.util.stream.StreamSupport; /** * Mod资源管理器 + * @author Zane */ public class ModAssetsManager { diff --git a/app/src/main/java/com/zane/smapiinstaller/ui/about/AboutFragment.java b/app/src/main/java/com/zane/smapiinstaller/ui/about/AboutFragment.java index 1e8b3f1..93341a8 100644 --- a/app/src/main/java/com/zane/smapiinstaller/ui/about/AboutFragment.java +++ b/app/src/main/java/com/zane/smapiinstaller/ui/about/AboutFragment.java @@ -4,20 +4,16 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.didikee.donate.AlipayDonate; -import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.ImageView; import android.widget.Toast; -import com.afollestad.materialdialogs.MaterialDialog; import com.microsoft.appcenter.crashes.Crashes; import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.constant.Constants; -import com.zane.smapiinstaller.constant.DialogAction; import com.zane.smapiinstaller.logic.CommonLogic; import com.zane.smapiinstaller.utils.DialogUtils; @@ -25,7 +21,6 @@ import androidx.fragment.app.Fragment; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; -import java9.util.function.BiConsumer; /** * @author Zane diff --git a/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigViewModel.java b/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigViewModel.java index fb7dd26..2744858 100644 --- a/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigViewModel.java +++ b/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigViewModel.java @@ -8,7 +8,6 @@ import com.hjq.language.LanguagesManager; import com.zane.smapiinstaller.MainApplication; import com.zane.smapiinstaller.constant.AppConfigKey; import com.zane.smapiinstaller.entity.AppConfig; -import com.zane.smapiinstaller.entity.AppConfigDao; import com.zane.smapiinstaller.entity.DaoSession; import com.zane.smapiinstaller.entity.ModManifestEntry; import com.zane.smapiinstaller.entity.TranslationResult; diff --git a/app/src/main/java/com/zane/smapiinstaller/ui/download/DownloadableContentAdapter.java b/app/src/main/java/com/zane/smapiinstaller/ui/download/DownloadableContentAdapter.java index 50e8e9b..f892fc3 100644 --- a/app/src/main/java/com/zane/smapiinstaller/ui/download/DownloadableContentAdapter.java +++ b/app/src/main/java/com/zane/smapiinstaller/ui/download/DownloadableContentAdapter.java @@ -93,16 +93,23 @@ public class DownloadableContentAdapter extends RecyclerView.Adapter CommonLogic.showProgressDialog(root, context, (dialog)->{ + ApkPatcher patcher = new ApkPatcher(context); + patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(root, dialog, null, progress)); + DialogUtils.setProgressDialogState(root, dialog, R.string.extracting_package, null); + String path = patcher.extract(1); + 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, 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, 6); + modAssetsManager.installDefaultMods(); + DialogUtils.setProgressDialogState(root, dialog, R.string.patching_package, 8); + if (!patcher.patch(path, true)) { + int target = patcher.getSwitchAction().getAndSet(0); + if (target == R.string.menu_download) { + DialogUtils.showConfirmDialog(root, 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) -> { + if (which == DialogAction.POSITIVE) { + NavController controller = Navigation.findNavController(installButton); + controller.navigate(InstallFragmentDirections.actionNavInstallToNavDownload()); + } + }); + } else { + DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game))); + } + return; + } + 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, null); + patcher.install(signPath); + })); + task.start(); + } + + /** + * 初始化逻辑 + */ + private void initialLogic() { + if (task != null) { + task.interrupt(); + } + task = new Thread(() -> CommonLogic.showProgressDialog(root, context, (dialog)->{ + ApkPatcher patcher = new ApkPatcher(context); + patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(root, dialog, null, progress)); + DialogUtils.setProgressDialogState(root, dialog, R.string.extracting_package, null); + String path = patcher.extract(0); + if (path == null) { + DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found))); + } + })); + task.start(); + } + /** * 安装逻辑 */ private void installLogic() { - AtomicReference dialogHolder = DialogUtils.showProgressDialog(root, R.string.install_progress_title, context.getString(R.string.extracting_package)); if (task != null) { task.interrupt(); } - task = new Thread(() -> { - ProgressDialog dialog = null; - try { - do { - Thread.sleep(10); - dialog = dialogHolder.get(); - } while (dialog == null); - ApkPatcher patcher = new ApkPatcher(context); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - ConfigManager configManager = new ConfigManager(); - if(configManager.getConfig().isInitial()) { - configManager.getConfig().setInitial(false); - configManager.getConfig().setDisableMonoMod(true); - configManager.flushConfig(); - } - } - ProgressDialog finalDialog = dialog; - patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(root, finalDialog, 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, 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, 6); - modAssetsManager.installDefaultMods(); - 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) { - DialogUtils.showConfirmDialog(root, 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) -> { - if (which == DialogAction.POSITIVE) { - NavController controller = Navigation.findNavController(installButton); - controller.navigate(InstallFragmentDirections.actionNavInstallToNavDownload()); - } - }); - } else { - DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game))); - } - return; - } - 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, null); - patcher.install(signPath); - } catch (InterruptedException ignored) { - } catch (Exception e) { - Crashes.trackError(e); - DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage()); - } finally { - DialogUtils.dismissDialog(root, dialog); + task = new Thread(() -> CommonLogic.showProgressDialog(root, context, (dialog)-> { + ApkPatcher patcher = new ApkPatcher(context); + 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, 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, 6); + modAssetsManager.installDefaultMods(); + 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) { + DialogUtils.showConfirmDialog(root, 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) -> { + if (which == DialogAction.POSITIVE) { + NavController controller = Navigation.findNavController(installButton); + controller.navigate(InstallFragmentDirections.actionNavInstallToNavDownload()); + } + }); + } else { + DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game))); + } + return; + } + 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, null); + patcher.install(signPath); + })); task.start(); } 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 36cfccd..2cc77fa 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/DialogUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/DialogUtils.java @@ -8,7 +8,6 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.input.DialogInputExtKt; import com.afollestad.materialdialogs.list.DialogListExtKt; import com.afollestad.materialdialogs.list.DialogSingleChoiceExtKt; -import com.afollestad.materialdialogs.message.DialogMessageSettings; import com.lmntrx.android.library.livin.missme.ProgressDialog; import com.microsoft.appcenter.crashes.Crashes; import com.zane.smapiinstaller.R; diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/FileUtils.java b/app/src/main/java/com/zane/smapiinstaller/utils/FileUtils.java index 34bd06e..5181874 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/FileUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/FileUtils.java @@ -8,7 +8,10 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; +import com.google.common.io.Files; import com.hjq.language.LanguagesManager; +import com.microsoft.appcenter.crashes.Crashes; +import com.smart.library.util.bspatch.BSPatchUtil; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; @@ -24,11 +27,13 @@ import java.nio.charset.StandardCharsets; /** * 文件工具类 + * * @author Zane */ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取文本文件 + * * @param file 文件 * @return 文本 */ @@ -45,6 +50,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取本地资源或Asset资源 + * * @param context context * @param filename 文件名 * @return 输入流 @@ -60,7 +66,8 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 尝试获取本地化后的资源文件 - * @param context context + * + * @param context context * @param filename 文件名 * @return 输入流 * @throws IOException 异常 @@ -74,8 +81,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { return new BOMInputStream(new FileInputStream(file)); } return context.getAssets().open(localedFilename); - } - catch (IOException e) { + } catch (IOException e) { Log.d("LOCALE", "No locale asset found", e); } return getLocalAsset(context, filename); @@ -83,6 +89,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取JSON文件 + * * @param file 文件 * @param type 数据类型 * @param 泛型类型 @@ -101,6 +108,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取JSON文件 + * * @param file 文件 * @param tClass 数据类型 * @param 泛型类型 @@ -119,6 +127,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 写入JSON文件到本地 + * * @param context context * @param filename 文件名 * @param content 内容 @@ -132,7 +141,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { writer.write(JsonUtil.toJson(content)); } finally { File distFile = new File(context.getFilesDir(), filename); - if(distFile.exists()) { + if (distFile.exists()) { org.zeroturnaround.zip.commons.FileUtils.forceDelete(distFile); } org.zeroturnaround.zip.commons.FileUtils.moveFile(file, distFile); @@ -143,12 +152,13 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 写入JSON文件到本地 - * @param file 文件 - * @param content 内容 + * + * @param file 文件 + * @param content 内容 */ public static void writeFileJson(File file, Object content) { try { - if(!file.getParentFile().exists()) { + if (!file.getParentFile().exists()) { org.zeroturnaround.zip.commons.FileUtils.forceMkdir(file.getParentFile()); } String filename = file.getName(); @@ -158,7 +168,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { writer.write(JsonUtil.toJson(content)); } finally { - if(file.exists()) { + if (file.exists()) { org.zeroturnaround.zip.commons.FileUtils.forceDelete(file); } org.zeroturnaround.zip.commons.FileUtils.moveFile(fileTmp, file); @@ -169,7 +179,8 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取资源文本 - * @param context context + * + * @param context context * @param filename 文件名 * @return 文本 */ @@ -186,7 +197,8 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取本地化后的资源文本 - * @param context context + * + * @param context context * @param filename 文件名 * @return 文本 */ @@ -203,6 +215,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取JSON资源 + * * @param context context * @param filename 资源名 * @param tClass 数据类型 @@ -211,7 +224,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { */ public static T getAssetJson(Context context, String filename, Class tClass) { String text = getAssetText(context, filename); - if(text != null){ + if (text != null) { return JsonUtil.fromJson(text, tClass); } return null; @@ -230,6 +243,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取JSON资源 + * * @param context context * @param filename 资源名 * @param type 数据类型 @@ -238,7 +252,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { */ public static T getAssetJson(Context context, String filename, TypeReference type) { String text = getAssetText(context, filename); - if(text != null){ + if (text != null) { return JsonUtil.fromJson(text, type); } return null; @@ -246,6 +260,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 读取资源为字节数组 + * * @param context context * @param filename 文件名 * @return 字节数组 @@ -262,6 +277,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 简化路径前缀 + * * @param path 文件路径 * @return 移除前缀后的路径 */ @@ -271,6 +287,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 计算资源文件SHA3-256 + * * @param context context * @param filename 资源名 * @return SHA3-256值 @@ -285,6 +302,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { /** * 计算文件SHA3-256 + * * @param file 文件 * @return SHA3-256值 */ @@ -295,4 +313,27 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils { } return null; } + + public static byte[] patchFile(byte[] originBytes, byte[] patchBytes) { + File patch = null; + File origin = null; + File patched = null; + try { + patch = File.createTempFile("patch", null); + Files.write(patchBytes, patch); + origin = File.createTempFile("origin", null); + Files.write(originBytes, origin); + patched = File.createTempFile("patched", null); + if (BSPatchUtil.bspatch(origin.getAbsolutePath(), patched.getAbsolutePath(), patch.getAbsolutePath()) == 0) { + return Files.asByteSource(patched).read(); + } + } catch (Exception e) { + Crashes.trackError(e); + } finally { + FileUtils.deleteQuietly(patch); + FileUtils.deleteQuietly(origin); + FileUtils.deleteQuietly(patched); + } + return null; + } } 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 5dda508..54021fc 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/ZipUtils.java @@ -67,6 +67,31 @@ public class ZipUtils { } } + public static void removeEntries(String inputZipFilename, String prefix, 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"); + } + 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(outFile))) { + for (ZioEntry inEntry : input.entries.values()) { + if (!inEntry.getName().startsWith(prefix)) { + zipOutput.write(inEntry); + } + index++; + if(index % reportInterval == 0) { + progressCallback.accept((int) (index * 100.0 / size)); + } + } + progressCallback.accept(100); + } + } + } + @Data @AllArgsConstructor public static class ZipEntrySource { diff --git a/app/src/main/res/layout/fragment_install.xml b/app/src/main/res/layout/fragment_install.xml index 296ed14..46b0c11 100644 --- a/app/src/main/res/layout/fragment_install.xml +++ b/app/src/main/res/layout/fragment_install.xml @@ -16,6 +16,28 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + +