diff --git a/app/build.gradle b/app/build.gradle index 4399f4b..5c4a2be 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,13 @@ android { buildTypes { release { - minifyEnabled false - shrinkResources false + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -53,11 +58,15 @@ dependencies { implementation group: 'com.google.guava', name: 'guava', version: '28.2-android' // https://mvnrepository.com/artifact/org.zeroturnaround/zt-zip implementation group: 'org.zeroturnaround', name: 'zt-zip', version: '1.14' - // https://mvnrepository.com/artifact/com.google.code.gson/gson - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6' // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.9' + // https://mvnrepository.com/artifact/commons-io/commons-io + implementation group: 'commons-io', name: 'commons-io', version: '2.6' implementation 'com.lzy.net:okgo:3.0.4' + // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.10.3' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.3' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.10.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c4d602b..ba298b3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,9 +19,9 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile --keepnames class com.path.to.your.ParcelableArg --keepnames class com.path.to.your.SerializableArg --keepnames class com.path.to.your.EnumArg +#-keepnames class com.path.to.your.ParcelableArg +#-keepnames class com.path.to.your.SerializableArg +#-keepnames class com.path.to.your.EnumArg -dontwarn javax.lang.model.element.Modifier @@ -118,34 +118,15 @@ static *** getCurrentEnvironment (...); } -##---------------Begin: proguard configuration for Gson ---------- -# Gson uses generic type information stored in a class file when working with fields. Proguard -# removes such information by default, so configure it to keep all of it. +# For Jackson -keepattributes Signature - -# For using GSON @Expose annotation -keepattributes *Annotation* +-keep class sun.misc.Unsafe { *; } +-dontwarn org.codehaus.jackson.** +-dontwarn com.fasterxml.jackson.databind.** +-keep class org.codehaus.jackson.** { *;} +-keep class com.fasterxml.jackson.** { *; } -# Gson specific classes --dontwarn sun.misc.** -#-keep class com.google.gson.stream.** { *; } - -# Application classes that will be serialized/deserialized over Gson --keep class com.google.gson.examples.android.model.** { ; } - -# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, -# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) --keep class * implements com.google.gson.TypeAdapter --keep class * implements com.google.gson.TypeAdapterFactory --keep class * implements com.google.gson.JsonSerializer --keep class * implements com.google.gson.JsonDeserializer - -# Prevent R8 from leaving Data object members always null --keepclassmembers,allowobfuscation class * { - @com.google.gson.annotations.SerializedName ; -} - -##---------------End: proguard configuration for Gson ---------- #okhttp -dontwarn okhttp3.** -keep class okhttp3.**{*;} diff --git a/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java b/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java index 7fcbbb9..32e55b6 100644 --- a/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java +++ b/app/src/main/java/com/zane/smapiinstaller/entity/ModManifestEntry.java @@ -13,4 +13,7 @@ public class ModManifestEntry { private String Description; private Set Dependencies; private ModManifestEntry ContentPackFor; + + private String MinimumVersion; + private Boolean IsRequired; } 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 7899dc5..055c7fb 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java @@ -10,10 +10,10 @@ import android.os.Build; import android.os.Environment; import android.util.Log; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.io.Files; -import com.google.gson.reflect.TypeToken; import com.zane.smapiinstaller.BuildConfig; import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.constant.Constants; @@ -59,8 +59,7 @@ public class ApkPatcher { public String extract() { PackageManager packageManager = context.getPackageManager(); - List packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeToken>() { - }.getType()); + List packageNames = FileUtils.getAssetJson(context, "package_names.json", new TypeReference>() { }); if (packageNames == null) { errorMessage.set(context.getString(R.string.error_game_not_found)); return null; 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 9a965c1..71f668d 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java @@ -10,10 +10,10 @@ import android.util.Log; import android.view.View; import com.afollestad.materialdialogs.MaterialDialog; +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.google.gson.reflect.TypeToken; import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.entity.ApkFilesManifest; import com.zane.smapiinstaller.entity.ManifestEntry; @@ -131,8 +131,7 @@ public class CommonLogic { } public static boolean unpackSmapiFiles(Context context, String apkPath, boolean checkMod) { - List manifestEntries = com.zane.smapiinstaller.utils.FileUtils.getAssetJson(context, "smapi_files_manifest.json", new TypeToken>() { - }.getType()); + List manifestEntries = com.zane.smapiinstaller.utils.FileUtils.getAssetJson(context, "smapi_files_manifest.json", new TypeReference>() { }); if (manifestEntries == null) return false; File basePath = new File(Environment.getExternalStorageDirectory() + "/StardewValley/"); diff --git a/app/src/main/java/com/zane/smapiinstaller/logic/DownloadabeContentManager.java b/app/src/main/java/com/zane/smapiinstaller/logic/DownloadabeContentManager.java index 5285e55..8a24652 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/DownloadabeContentManager.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/DownloadabeContentManager.java @@ -2,13 +2,13 @@ package com.zane.smapiinstaller.logic; import android.view.View; -import com.google.gson.Gson; import com.lzy.okgo.OkGo; import com.lzy.okgo.callback.StringCallback; import com.lzy.okgo.model.Response; import com.zane.smapiinstaller.constant.Constants; import com.zane.smapiinstaller.entity.DownloadableContentList; import com.zane.smapiinstaller.utils.FileUtils; +import com.zane.smapiinstaller.utils.JSONUtil; public class DownloadabeContentManager { @@ -28,8 +28,8 @@ public class DownloadabeContentManager { OkGo.get(Constants.DLC_LIST_UPDATE_URL).execute(new StringCallback(){ @Override public void onSuccess(Response response) { - DownloadableContentList content = new Gson().fromJson(response.body(), DownloadableContentList.class); - if(downloadableContentList.getVersion() < content.getVersion()) { + DownloadableContentList content = JSONUtil.fromJson(response.body(), DownloadableContentList.class); + if(content != null && downloadableContentList.getVersion() < content.getVersion()) { FileUtils.writeAssetJson(root.getContext(), "downloadable_content_list.json", content); downloadableContentList = content; } 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 487e995..ab75df3 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ModAssetsManager.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ModAssetsManager.java @@ -6,6 +6,7 @@ import android.util.Log; import android.view.View; import com.afollestad.materialdialogs.DialogAction; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; @@ -14,7 +15,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimaps; import com.google.common.collect.Queues; -import com.google.gson.reflect.TypeToken; import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.constant.Constants; import com.zane.smapiinstaller.entity.ModManifestEntry; @@ -51,8 +51,7 @@ public class ModAssetsManager { boolean foundManifest = false; for (File file : currentFile.listFiles(File::isFile)) { if (StringUtils.equalsIgnoreCase(file.getName(), "manifest.json")) { - ModManifestEntry manifest = FileUtils.getFileJson(file, new TypeToken() { - }.getType()); + ModManifestEntry manifest = FileUtils.getFileJson(file, ModManifestEntry.class); foundManifest = true; if (manifest != null) { manifest.setAssetPath(file.getParentFile().getAbsolutePath()); @@ -81,8 +80,7 @@ public class ModAssetsManager { boolean foundManifest = false; for (File file : currentFile.listFiles(File::isFile)) { if (StringUtils.equalsIgnoreCase(file.getName(), "manifest.json")) { - ModManifestEntry manifest = FileUtils.getFileJson(file, new TypeToken() { - }.getType()); + ModManifestEntry manifest = FileUtils.getFileJson(file, ModManifestEntry.class); foundManifest = true; if (manifest != null) { manifest.setAssetPath(file.getParentFile().getAbsolutePath()); @@ -101,8 +99,7 @@ public class ModAssetsManager { public boolean installDefaultMods() { Activity context = CommonLogic.getActivityFromView(root); - List modManifestEntries = FileUtils.getAssetJson(context, "mods_manifest.json", new TypeToken>() { - }.getType()); + List modManifestEntries = FileUtils.getAssetJson(context, "mods_manifest.json", new TypeReference>() { }); if (modManifestEntries == null) return false; File modFolder = new File(Environment.getExternalStorageDirectory(), Constants.MOD_PATH); @@ -179,6 +176,9 @@ public class ModAssetsManager { Iterable dependencyErrors = Iterables.filter(Iterables.transform(installedModMap.values(), mod -> { if (mod.getDependencies() != null) { ArrayList unsatisfiedDependencies = Lists.newArrayList(Iterables.filter(mod.getDependencies(), dependency -> { + if(dependency.getIsRequired() != null && !dependency.getIsRequired()) { + return false; + } ImmutableList entries = installedModMap.get(dependency.getUniqueID()); if (entries.size() != 1) return true; @@ -186,10 +186,10 @@ public class ModAssetsManager { if (StringUtils.isBlank(version)) { return true; } - if (StringUtils.isBlank(dependency.getVersion())) { + if (StringUtils.isBlank(dependency.getMinimumVersion())) { return false; } - if (VersionUtil.compareVersion(version, dependency.getVersion()) < 0) { + if (VersionUtil.compareVersion(version, dependency.getMinimumVersion()) < 0) { return true; } return false; @@ -219,16 +219,19 @@ public class ModAssetsManager { Iterable dependencyErrors = Iterables.filter(Iterables.transform(installedModMap.values(), mod -> { ModManifestEntry dependency = mod.getContentPackFor(); if (dependency != null) { + if(dependency.getIsRequired() != null && !dependency.getIsRequired()) { + return null; + } ImmutableList entries = installedModMap.get(dependency.getUniqueID()); if (entries.size() != 1) return root.getContext().getString(R.string.error_depends_on_mod, mod.getUniqueID(), dependency.getUniqueID()); String version = entries.get(0).getVersion(); if (!StringUtils.isBlank(version)) { - if (StringUtils.isBlank(dependency.getVersion())) { + if (StringUtils.isBlank(dependency.getMinimumVersion())) { return null; } - if (VersionUtil.compareVersion(version, dependency.getVersion()) < 0) { - return root.getContext().getString(R.string.error_depends_on_mod, mod.getUniqueID(), dependency.getUniqueID()); + if (VersionUtil.compareVersion(version, dependency.getMinimumVersion()) < 0) { + return root.getContext().getString(R.string.error_depends_on_mod_version, mod.getUniqueID(), dependency.getUniqueID(), dependency.getMinimumVersion()); } } return null; diff --git a/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigEditFragment.java b/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigEditFragment.java index 50c56c9..c15f467 100644 --- a/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigEditFragment.java +++ b/app/src/main/java/com/zane/smapiinstaller/ui/config/ConfigEditFragment.java @@ -7,10 +7,10 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; -import com.google.gson.Gson; import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.logic.CommonLogic; import com.zane.smapiinstaller.utils.FileUtils; +import com.zane.smapiinstaller.utils.JSONUtil; import java.io.File; import java.io.FileOutputStream; @@ -54,7 +54,7 @@ public class ConfigEditFragment extends Fragment { } @OnClick(R.id.button_config_save) void onConfigSave() { try { - new Gson().fromJson(editText.getText().toString(), Object.class); + JSONUtil.checkJson(editText.getText().toString()); FileOutputStream outputStream = new FileOutputStream(configPath); try(OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)){ outputStreamWriter.write(editText.getText().toString()); 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 cf90c48..2c4b9ab 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/FileUtils.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/FileUtils.java @@ -2,13 +2,14 @@ package com.zane.smapiinstaller.utils; import android.content.Context; import android.os.Environment; +import android.util.Log; +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.gson.Gson; -import com.google.gson.GsonBuilder; +import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; import java.io.File; @@ -36,18 +37,16 @@ public class FileUtils { public static InputStream getLocalAsset(Context context, String filename) throws IOException { File file = new File(context.getFilesDir(), filename); if (file.exists()) { - return new FileInputStream(file); + return new BOMInputStream(new FileInputStream(file)); } return context.getAssets().open(filename); } - public static T getFileJson(File file, Type type) { + public static T getFileJson(File file, TypeReference type) { try { InputStream inputStream = new FileInputStream(file); - try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setLenient(); - return gsonBuilder.create().fromJson(CharStreams.toString(reader), type); + try (InputStreamReader reader = new InputStreamReader(new BOMInputStream(inputStream), StandardCharsets.UTF_8)) { + return JSONUtil.fromJson(CharStreams.toString(reader), type); } } catch (Exception ignored) { } @@ -57,10 +56,8 @@ public class FileUtils { public static T getFileJson(File file, Class tClass) { try { InputStream inputStream = new FileInputStream(file); - try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setLenient(); - return gsonBuilder.create().fromJson(CharStreams.toString(reader), tClass); + try (InputStreamReader reader = new InputStreamReader(new BOMInputStream(inputStream), StandardCharsets.UTF_8)) { + return JSONUtil.fromJson(CharStreams.toString(reader), tClass); } } catch (Exception ignored) { } @@ -73,7 +70,7 @@ public class FileUtils { File file = new File(context.getFilesDir(), tmpFilename); FileOutputStream outputStream = new FileOutputStream(file); try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { - writer.write(new Gson().toJson(content)); + writer.write(JSONUtil.toJson(content)); } finally { org.zeroturnaround.zip.commons.FileUtils.moveFile(file, new File(context.getFilesDir(), filename)); } @@ -85,18 +82,18 @@ public class FileUtils { try { InputStream inputStream = getLocalAsset(context, filename); try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { - return new Gson().fromJson(CharStreams.toString(reader), tClass); + return JSONUtil.fromJson(CharStreams.toString(reader), tClass); } } catch (IOException ignored) { } return null; } - public static T getAssetJson(Context context, String filename, Type type) { + public static T getAssetJson(Context context, String filename, TypeReference type) { try { InputStream inputStream = getLocalAsset(context, filename); try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { - return new Gson().fromJson(CharStreams.toString(reader), type); + return JSONUtil.fromJson(CharStreams.toString(reader), type); } } catch (IOException ignored) { } diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/JSONUtil.java b/app/src/main/java/com/zane/smapiinstaller/utils/JSONUtil.java new file mode 100644 index 0000000..06f3e28 --- /dev/null +++ b/app/src/main/java/com/zane/smapiinstaller/utils/JSONUtil.java @@ -0,0 +1,44 @@ +package com.zane.smapiinstaller.utils; + +import android.util.Log; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JSONUtil { + private static final ObjectMapper mapper = new ObjectMapper(); + static { + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); + mapper.configure(JsonReadFeature.ALLOW_TRAILING_COMMA.mappedFeature(), true); + mapper.configure(JsonReadFeature.ALLOW_MISSING_VALUES.mappedFeature(), true); + mapper.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS.mappedFeature(), true); + } + public static String toJson(Object object) throws Exception { + return mapper.writeValueAsString(object); + } + + public static void checkJson(String jsonString) throws JsonProcessingException { + mapper.readValue(jsonString, Object.class); + } + + public static T fromJson(String jsonString, Class cls) { + try { + return mapper.readValue(jsonString, cls); + } catch (JsonProcessingException e) { + Log.e("JSON", "Deserialize error", e); + } + return null; + } + + public static T fromJson(String jsonString, TypeReference type) { + try { + return mapper.readValue(jsonString, type); + } catch (JsonProcessingException e) { + Log.e("JSON", "Deserialize error", e); + } + return null; + } +} diff --git a/app/src/main/java/com/zane/smapiinstaller/utils/VersionUtil.java b/app/src/main/java/com/zane/smapiinstaller/utils/VersionUtil.java index d49d289..1838980 100644 --- a/app/src/main/java/com/zane/smapiinstaller/utils/VersionUtil.java +++ b/app/src/main/java/com/zane/smapiinstaller/utils/VersionUtil.java @@ -4,22 +4,41 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; import java.util.List; public class VersionUtil { - private static int parseVersionSection(String version) { + private static int compareVersionSection(String sectionA, String sectionB) { try { - return Integer.parseInt(version); + return Integer.compare(Integer.parseInt(sectionA), Integer.parseInt(sectionB)); } catch (Exception ignored) { } - List list = Splitter.on("-").splitToList(version); - switch (list.get(0).toLowerCase()) { - case "alpha": - return -2; - case "beta": + List listA = Splitter.on("-").splitToList(sectionA); + List listB = Splitter.on("-").splitToList(sectionB); + int i; + for (i = 0; i < listA.size() && i < listB.size(); i++) { + Integer intA = null; + Integer intB = null; + try { + intA = Integer.parseInt(listA.get(i)); + return Integer.compare(intA, Integer.parseInt(listB.get(i))); + } catch (Exception ignored) { + try { + intB = Integer.parseInt(listB.get(i)); + } catch (Exception ignored2) { + } + } + if(StringUtils.equals(listA.get(i), listB.get(i))) + continue; + if(intA != null && intB == null) + return 1; + else if(intA == null) return -1; + return listA.get(i).compareTo(listB.get(i)); } - return 0; + return Integer.compare(listA.size(), listB.size()); } private static boolean isZero(List versionSections) { return !Iterables.filter(versionSections, version -> { @@ -44,10 +63,16 @@ public class VersionUtil { } return 1; } - int compare = Integer.compare(parseVersionSection(versionSectionsA.get(i)), parseVersionSection(versionSectionsB.get(i))); + int compare = compareVersionSection(versionSectionsA.get(i), versionSectionsB.get(i)); if(compare != 0) return compare; } + if(versionSectionsA.size() < versionSectionsB.size()) { + if(isZero(versionSectionsB.subList(versionSectionsA.size(), versionSectionsB.size()))) { + return 0; + } + return -1; + } return 0; } } diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index ea36f67..c6b87fe 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -53,4 +53,5 @@ 正在下載: %d KB / %d KB 關於 你的系統版本過老,可能會導致0Harmony無效,建議升級到安卓6及以上版本 + The %s is depends on %s %s version or later, please update it first diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ea36f67..c6b87fe 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -53,4 +53,5 @@ 正在下載: %d KB / %d KB 關於 你的系統版本過老,可能會導致0Harmony無效,建議升級到安卓6及以上版本 + The %s is depends on %s %s version or later, please update it first diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index b961278..da14565 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -53,4 +53,5 @@ 正在下载: %d KB / %d KB 关于 你的系统版本过老,可能会导致0Harmony无效,建议升级到安卓6及以上版本 + %s依赖%s %s版本,请先更新它 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9db7f2b..dc795e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,5 @@ Hello blank fragment About You device system version is too old for MonoMod, this may leads to crash of 0Harmony framework, update to Android M or later if possible + The %s is depends on %s %s version or later, please update it first