1.Privacy Policy

2.Layout adjust
3.Amazon Store version compat
4.Thai translation
5.Advanced patch mode
This commit is contained in:
ZaneYork 2020-05-23 17:53:58 +08:00
parent 263649cb0b
commit a9af520e0d
31 changed files with 415 additions and 143 deletions

View File

@ -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'
}

View File

@ -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.** { *; }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
}
]
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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);
}

View File

@ -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";
}

View File

@ -2,6 +2,9 @@ package com.zane.smapiinstaller.dto;
import lombok.Data;
/**
* @author Zane
*/
@Data
public class AppUpdateCheckResultDto {
/**

View File

@ -28,4 +28,14 @@ public class ManifestEntry {
* 文件是否不属于兼容包中
*/
private boolean external;
/**
* 补丁CRC
*/
private String patchCrc;
/**
* 是否为高级模式补丁
*/
private boolean advanced;
}

View File

@ -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<String> packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference<List<String>>() {
@ -108,12 +112,31 @@ public class ApkPatcher {
}
}
File distFile = new File(dest, apkFile.getName());
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<ManifestEntry> manifestEntries = apkFilesManifest.getManifestEntries();
errorMessage.set(null);
List<ZipUtils.ZipEntrySource> entries = StreamSupport.stream(manifestEntries).map(entry -> {
if (entry.isExternal()) {
return new ZipUtils.ZipEntrySource(entry.getTargetPath(), FileUtils.getAssetBytes(context, apkFilesManifest.getBasePath() + entry.getAssetPath()), entry.getCompression());
} else {
return new ZipUtils.ZipEntrySource(entry.getTargetPath(), FileUtils.getAssetBytes(context, entry.getAssetPath()), entry.getCompression());
if(entry.isAdvanced() && !advanced) {
return null;
}
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;
}
}).collect(Collectors.toList());
entries.add(new ZipUtils.ZipEntrySource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
emitProgress(10);
String patchedFilename = apkPath + ".patched";
@ -191,7 +242,7 @@ public class ApkPatcher {
AtomicReference<String> versionName = new AtomicReference<>();
AtomicLong versionCode = new AtomicLong();
Predicate<ManifestTagVisitor.AttrArgs> 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;
}

View File

@ -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<ProgressDialog> dialogConsumer) {
AtomicReference<ProgressDialog> 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);
}
}
}

View File

@ -47,6 +47,7 @@ import java9.util.stream.StreamSupport;
/**
* Mod资源管理器
* @author Zane
*/
public class ModAssetsManager {

View File

@ -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

View File

@ -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;

View File

@ -93,16 +93,23 @@ public class DownloadableContentAdapter extends RecyclerView.Adapter<Downloadabl
typeTextView.setText(downloadableContent.getType());
nameTextView.setText(downloadableContent.getName());
descriptionTextView.setText(downloadableContent.getDescription());
if (StringUtils.isBlank(downloadableContent.getAssetPath())) {
buttonRemove.setVisibility(View.INVISIBLE);
buttonDownload.setVisibility(View.VISIBLE);
} else {
if (StringUtils.isNoneBlank(downloadableContent.getAssetPath())) {
File contentFile = new File(itemView.getContext().getFilesDir(), downloadableContent.getAssetPath());
if (contentFile.exists()) {
Context context = itemView.getContext();
File file = new File(context.getCacheDir(), downloadableContent.getName() + ".zip");
if (!file.exists() || !StringUtils.equalsIgnoreCase(FileUtils.getFileHash(file), downloadableContent.getHash())) {
buttonRemove.setVisibility(View.VISIBLE);
buttonDownload.setVisibility(View.VISIBLE);
return;
}
buttonRemove.setVisibility(View.VISIBLE);
buttonDownload.setVisibility(View.INVISIBLE);
return;
}
}
buttonRemove.setVisibility(View.INVISIBLE);
buttonDownload.setVisibility(View.VISIBLE);
}
public ViewHolder(View view) {
@ -119,6 +126,8 @@ public class DownloadableContentAdapter extends RecyclerView.Adapter<Downloadabl
if (which == DialogAction.POSITIVE) {
try {
FileUtils.forceDelete(contentFile);
buttonDownload.setVisibility(View.VISIBLE);
buttonRemove.setVisibility(View.INVISIBLE);
} catch (IOException e) {
DialogUtils.showAlertDialog(itemView, R.string.error, e.getLocalizedMessage());
}
@ -203,6 +212,8 @@ public class DownloadableContentAdapter extends RecyclerView.Adapter<Downloadabl
ZipUtil.unpack(downloadedFile, new File(context.getFilesDir(), downloadableContent.getAssetPath()));
}
DialogUtils.showAlertDialog(itemView, R.string.info, R.string.download_unpack_success);
buttonDownload.setVisibility(View.INVISIBLE);
buttonRemove.setVisibility(View.VISIBLE);
} catch (Exception e) {
DialogUtils.showAlertDialog(itemView, R.string.error, e.getLocalizedMessage());
}

View File

@ -8,18 +8,19 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.google.common.io.Files;
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.AppConfigKey;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.logic.ApkPatcher;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ConfigManager;
import com.zane.smapiinstaller.logic.ModAssetsManager;
import com.zane.smapiinstaller.utils.ConfigUtils;
import com.zane.smapiinstaller.utils.DialogUtils;
import org.apache.commons.lang3.RegExUtils;
@ -28,7 +29,6 @@ import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
@ -55,15 +55,22 @@ public class InstallFragment extends Fragment {
@BindView(R.id.text_latest_running)
TextView textLatestRunning;
@BindView(R.id.layout_adv_install)
LinearLayout layoutAdvInstall;
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
root = inflater.inflate(R.layout.fragment_install, container, false);
ButterKnife.bind(this, root);
context = this.getActivity();
if (Boolean.parseBoolean(ConfigUtils.getConfig((MainApplication) context.getApplication(), AppConfigKey.ADVANCED_MODE, "false").getValue())) {
installButton.setVisibility(View.INVISIBLE);
layoutAdvInstall.setVisibility(View.VISIBLE);
}
try {
String firstLine = Files.asCharSource(new File(Environment.getExternalStorageDirectory(), Constants.LOG_PATH), StandardCharsets.UTF_8).readFirstLine();
if(StringUtils.isNoneBlank(firstLine)) {
if (StringUtils.isNoneBlank(firstLine)) {
String versionString = RegExUtils.removePattern(firstLine, "\\[.+\\]\\s+");
versionString = RegExUtils.removePattern(versionString, "\\s+with.+");
textLatestRunning.setText(context.getString(R.string.smapi_version_runing, versionString));
@ -87,32 +94,89 @@ public class InstallFragment extends Fragment {
}
}
@OnClick(R.id.button_adv_initial)
void advInitial() {
initialLogic();
}
@OnClick(R.id.button_adv_install)
void advInstall() {
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(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<ProgressDialog> 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);
task = new Thread(() -> CommonLogic.showProgressDialog(root, context, (dialog)-> {
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));
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) {
@ -150,14 +214,7 @@ public class InstallFragment extends Fragment {
}
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.start();
}

View File

@ -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;

View File

@ -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,6 +66,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils {
/**
* 尝试获取本地化后的资源文件
*
* @param context context
* @param filename 文件名
* @return 输入流
@ -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 <T> 泛型类型
@ -101,6 +108,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils {
/**
* 读取JSON文件
*
* @param file 文件
* @param tClass 数据类型
* @param <T> 泛型类型
@ -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 内容
*/
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,6 +179,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils {
/**
* 读取资源文本
*
* @param context context
* @param filename 文件名
* @return 文本
@ -186,6 +197,7 @@ public class FileUtils extends org.zeroturnaround.zip.commons.FileUtils {
/**
* 读取本地化后的资源文本
*
* @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> T getAssetJson(Context context, String filename, Class<T> 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> T getAssetJson(Context context, String filename, TypeReference<T> 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;
}
}

View File

@ -67,6 +67,31 @@ public class ZipUtils {
}
}
public static void removeEntries(String inputZipFilename, String prefix, String outputZipFilename, Consumer<Integer> progressCallback) throws IOException {
File inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile)) {
throw new IllegalArgumentException("Input and output files are the same");
}
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 {

View File

@ -16,6 +16,28 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/layout_adv_install"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/button_adv_initial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_initial" />
<Button
android:id="@+id/button_adv_install"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_install" />
</LinearLayout>
<TextView
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -55,26 +77,26 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.1" />
app:layout_constraintGuide_begin="50dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_h2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.2" />
app:layout_constraintGuide_begin="100dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_h3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.3" />
app:layout_constraintGuide_begin="150dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_h4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.7" />
app:layout_constraintGuide_end="100dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -29,25 +29,30 @@
android:orderInCategory="103"
android:title="@string/settings_disable_mono_mod"
app:showAsAction="never" />
<item
android:id="@+id/settings_advanced_mode"
android:orderInCategory="104"
android:title="@string/settings_advanced_mode"
app:showAsAction="never" />
</group>
<item
android:id="@+id/settings_set_mod_path"
android:orderInCategory="103"
android:orderInCategory="105"
android:title="@string/settings_set_mod_path"
app:showAsAction="never" />
<item
android:id="@+id/settings_set_max_log_size"
android:orderInCategory="104"
android:orderInCategory="106"
android:title="@string/settings_set_max_log_size"
app:showAsAction="never" />
<item
android:id="@+id/settings_language"
android:orderInCategory="105"
android:orderInCategory="107"
android:title="@string/settings_set_language"
app:showAsAction="never" />
<item
android:id="@+id/settings_translation_service"
android:orderInCategory="106"
android:orderInCategory="108"
android:title="@string/settings_translation_service"
app:showAsAction="never" />
</menu>

View File

@ -76,4 +76,6 @@
<string name="settings_set_max_log_size">最大日誌檔案大小</string>
<string name="button_qq_group_text">QQ群</string>
<string name="privacy_policy">隱私權條款</string>
<string name="settings_advanced_mode">高級安裝模式</string>
<string name="button_initial">初始化</string>
</resources>

View File

@ -76,4 +76,6 @@
<string name="settings_set_max_log_size">最大日志大小</string>
<string name="button_qq_group_text">QQ群</string>
<string name="privacy_policy">隐私权政策</string>
<string name="settings_advanced_mode">高级安装模式</string>
<string name="button_initial">初始化</string>
</resources>

View File

@ -80,4 +80,6 @@
<string name="settings_set_max_log_size">Max Log Size</string>
<string name="button_qq_group_text">QQ Group</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="settings_advanced_mode">Advanced Patch Mode</string>
<string name="button_initial">Initial</string>
</resources>