Lombok/Bug fix/DLC(in progress)

This commit is contained in:
ZaneYork 2020-03-06 01:32:25 +08:00
parent ef0639797e
commit 97e62a4c8f
34 changed files with 538 additions and 199 deletions

8
.idea/compiler.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
</annotationProcessing>
</component>
</project>

View File

@ -47,6 +47,7 @@ dependencies {
implementation 'com.afollestad.material-dialogs:core:0.9.6.0' implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
// https://mvnrepository.com/artifact/com.jakewharton/butterknife // https://mvnrepository.com/artifact/com.jakewharton/butterknife
implementation 'com.jakewharton:butterknife:10.2.1' implementation 'com.jakewharton:butterknife:10.2.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1' annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
// https://mvnrepository.com/artifact/com.google.guava/guava // https://mvnrepository.com/artifact/com.google.guava/guava
implementation group: 'com.google.guava', name: 'guava', version: '28.2-android' implementation group: 'com.google.guava', name: 'guava', version: '28.2-android'
@ -54,9 +55,15 @@ dependencies {
implementation group: 'org.zeroturnaround', name: 'zt-zip', version: '1.14' implementation group: 'org.zeroturnaround', name: 'zt-zip', version: '1.14'
// https://mvnrepository.com/artifact/com.google.code.gson/gson // https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6' 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'
implementation 'com.lzy.net:okgo:3.0.4'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
compileOnly 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'
} }

View File

@ -146,6 +146,13 @@
} }
##---------------End: proguard configuration for Gson ---------- ##---------------End: proguard configuration for Gson ----------
#okhttp
-dontwarn okhttp3.**
-keep class okhttp3.**{*;}
#okio
-dontwarn okio.**
-keep class okio.**{*;}
-keep class com.zane.** { *; } -keep class com.zane.** { *; }
-keep class pxb.android.** { *; } -keep class pxb.android.** { *; }

View File

@ -2,12 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zane.smapiinstaller"> package="com.zane.smapiinstaller">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
android:name=".MainApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -1,62 +1,67 @@
[ {
{ "minBuildCode": 138,
"targetPath": "classes.dex", "maxBuildCode": null,
"assetPath": "apk/classes.dex", "withMonoMod": true,
"compression": 8 "manifestEntries": [
}, {
{ "targetPath": "classes.dex",
"targetPath": "res/mipmap-mdpi-v4/ic_launcher_foreground.png", "assetPath": "apk/classes.dex",
"assetPath": "apk/ic_launcher_foreground.png", "compression": 8
"compression": 8 },
}, {
{ "targetPath": "res/mipmap-mdpi-v4/ic_launcher_foreground.png",
"targetPath": "res/mipmap-hdpi-v4/ic_launcher_foreground.png", "assetPath": "apk/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png", "compression": 8
"compression": 8 },
}, {
{ "targetPath": "res/mipmap-hdpi-v4/ic_launcher_foreground.png",
"targetPath": "res/mipmap-xhdpi-v4/ic_launcher_foreground.png", "assetPath": "apk/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png", "compression": 8
"compression": 8 },
}, {
{ "targetPath": "res/mipmap-xhdpi-v4/ic_launcher_foreground.png",
"targetPath": "res/mipmap-xxhdpi-v4/ic_launcher_foreground.png", "assetPath": "apk/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png", "compression": 8
"compression": 8 },
}, {
{ "targetPath": "res/mipmap-xxhdpi-v4/ic_launcher_foreground.png",
"targetPath": "res/mipmap-xxxhdpi-v4/ic_launcher_foreground.png", "assetPath": "apk/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png", "compression": 8
"compression": 8 },
}, {
{ "targetPath": "res/mipmap-xxxhdpi-v4/ic_launcher_foreground.png",
"targetPath": "assemblies/Newtonsoft.Json.dll", "assetPath": "apk/ic_launcher_foreground.png",
"assetPath": "apk/Newtonsoft.Json.dll", "compression": 8
"compression": 0 },
}, {
{ "targetPath": "assemblies/Newtonsoft.Json.dll",
"targetPath": "assemblies/SMAPI.Toolkit.CoreInterfaces.dll", "assetPath": "apk/Newtonsoft.Json.dll",
"assetPath": "apk/SMAPI.Toolkit.CoreInterfaces.dll", "compression": 0
"compression": 0 },
}, {
{ "targetPath": "assemblies/SMAPI.Toolkit.CoreInterfaces.dll",
"targetPath": "assemblies/SMAPI.Toolkit.dll", "assetPath": "apk/SMAPI.Toolkit.CoreInterfaces.dll",
"assetPath": "apk/SMAPI.Toolkit.dll", "compression": 0
"compression": 0 },
}, {
{ "targetPath": "assemblies/SMAPI.Toolkit.dll",
"targetPath": "assemblies/StardewModdingAPI.dll", "assetPath": "apk/SMAPI.Toolkit.dll",
"assetPath": "apk/StardewModdingAPI.dll", "compression": 0
"compression": 0 },
}, {
{ "targetPath": "assemblies/StardewModdingAPI.dll",
"targetPath": "assemblies/System.Data.dll", "assetPath": "apk/StardewModdingAPI.dll",
"assetPath": "apk/System.Data.dll", "compression": 0
"compression": 0 },
}, {
{ "targetPath": "assemblies/System.Data.dll",
"targetPath": "assemblies/System.Numerics.dll", "assetPath": "apk/System.Data.dll",
"assetPath": "apk/System.Numerics.dll", "compression": 0
"compression": 0 },
} {
] "targetPath": "assemblies/System.Numerics.dll",
"assetPath": "apk/System.Numerics.dll",
"compression": 0
}
]
}

View File

@ -0,0 +1,17 @@
{
"version": 1,
"contents": [
{
"type": "COMPAT",
"name": "SMAPI for 1.4.5.135",
"description": "SMAPI compat package pack for game 1.4.5.135 - 1.4.5.137",
"url": "http://dl.zaneyork.cn"
},
{
"type": "COMPAT",
"name": "SMAPI for 1.4.4.122",
"description": "SMAPI compat package pack for game 1.4.4.122 - 1.4.4.128",
"url": "http://dl.zaneyork.cn"
}
]
}

View File

@ -2,7 +2,7 @@
{ {
"assetPath":"mods/virtual-keyboard.zip", "assetPath":"mods/virtual-keyboard.zip",
"Name": "VirtualKeyboard", "Name": "VirtualKeyboard",
"UniqueID": "SMAPI.VirtualKeyboard" "UniqueID": "VirtualKeyboard"
}, },
{ {
"assetPath":"mods/custom-localization.zip", "assetPath":"mods/custom-localization.zip",

View File

@ -0,0 +1,13 @@
package com.zane.smapiinstaller;
import android.app.Application;
import com.lzy.okgo.OkGo;
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
OkGo.getInstance().init(this);
}
}

View File

@ -0,0 +1,12 @@
package com.zane.smapiinstaller.entity;
import java.util.List;
import lombok.Data;
@Data
public class ApkFilesManifest {
private Long minBuildCode;
private Long maxBuildCode;
private List<ManifestEntry> manifestEntries;
}

View File

@ -0,0 +1,11 @@
package com.zane.smapiinstaller.entity;
import lombok.Data;
@Data
public class DownloadableContent {
private String type;
private String name;
private String url;
private String description;
}

View File

@ -0,0 +1,11 @@
package com.zane.smapiinstaller.entity;
import java.util.List;
import lombok.Data;
@Data
public class DownloadableContentList {
private int version;
List<DownloadableContent> contents;
}

View File

@ -1,40 +1,11 @@
package com.zane.smapiinstaller.entity; package com.zane.smapiinstaller.entity;
import lombok.Data;
@Data
public class ManifestEntry { public class ManifestEntry {
private String targetPath; private String targetPath;
private String assetPath; private String assetPath;
private int compression; private int compression;
private int origin; private int origin;
public String getTargetPath() {
return targetPath;
}
public void setTargetPath(String targetPath) {
this.targetPath = targetPath;
}
public String getAssetPath() {
return assetPath;
}
public void setAssetPath(String assetPath) {
this.assetPath = assetPath;
}
public int getCompression() {
return compression;
}
public void setCompression(int compression) {
this.compression = compression;
}
public int getOrigin() {
return origin;
}
public void setOrigin(int origin) {
this.origin = origin;
}
} }

View File

@ -1,41 +1,12 @@
package com.zane.smapiinstaller.entity; package com.zane.smapiinstaller.entity;
import lombok.Data;
@Data
public class ModManifestEntry { public class ModManifestEntry {
private String assetPath; private String assetPath;
private String Name; private String Name;
private String UniqueID; private String UniqueID;
private String Description; private String Description;
private ModManifestEntry ContentPackFor; private ModManifestEntry ContentPackFor;
public String getAssetPath() {
return assetPath;
}
public void setAssetPath(String assetPath) {
this.assetPath = assetPath;
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public String getUniqueID() {
return UniqueID;
}
public void setUniqueID(String uniqueID) {
UniqueID = uniqueID;
}
public String getDescription() {
return Description;
}
public void setDescription(String description) {
Description = description;
}
} }

View File

@ -11,16 +11,20 @@ import android.os.Environment;
import android.util.Log; import android.util.Log;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.zane.smapiinstaller.BuildConfig; import com.zane.smapiinstaller.BuildConfig;
import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants; import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.entity.ApkFilesManifest;
import com.zane.smapiinstaller.entity.ManifestEntry; import com.zane.smapiinstaller.entity.ManifestEntry;
import net.fornwall.apksigner.KeyStoreFileManager; import net.fornwall.apksigner.KeyStoreFileManager;
import net.fornwall.apksigner.ZipSigner; import net.fornwall.apksigner.ZipSigner;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.ByteSource; import org.zeroturnaround.zip.ByteSource;
import org.zeroturnaround.zip.ZipEntrySource; import org.zeroturnaround.zip.ZipEntrySource;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
@ -29,7 +33,6 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
@ -48,6 +51,8 @@ public class ApkPatcher {
private static final String TAG = "PATCHER"; private static final String TAG = "PATCHER";
private AtomicReference<String> errorMessage = new AtomicReference<>();
public ApkPatcher(Context context) { public ApkPatcher(Context context) {
this.context = context; this.context = context;
} }
@ -56,8 +61,10 @@ public class ApkPatcher {
PackageManager packageManager = context.getPackageManager(); PackageManager packageManager = context.getPackageManager();
List<String> packageNames = CommonLogic.getAssetJson(context, "package_names.json", new TypeToken<List<String>>() { List<String> packageNames = CommonLogic.getAssetJson(context, "package_names.json", new TypeToken<List<String>>() {
}.getType()); }.getType());
if (packageNames == null) if (packageNames == null) {
errorMessage.set(context.getString(R.string.error_game_not_found));
return null; return null;
}
for (String packageName : packageNames) { for (String packageName : packageNames) {
try { try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
@ -69,6 +76,7 @@ public class ApkPatcher {
File dest = new File(externalFilesDir.getAbsolutePath() + "/SMAPI Installer/"); File dest = new File(externalFilesDir.getAbsolutePath() + "/SMAPI Installer/");
if (!dest.exists()) { if (!dest.exists()) {
if (!dest.mkdir()) { if (!dest.mkdir()) {
errorMessage.set(String.format(context.getString(R.string.error_failed_to_create_file), dest.getAbsolutePath()));
return null; return null;
} }
} }
@ -78,6 +86,7 @@ public class ApkPatcher {
} }
} catch (PackageManager.NameNotFoundException | IOException e) { } catch (PackageManager.NameNotFoundException | IOException e) {
Log.e(TAG, "Extract error", e); Log.e(TAG, "Extract error", e);
errorMessage.set(e.getLocalizedMessage());
} }
} }
return null; return null;
@ -90,29 +99,36 @@ public class ApkPatcher {
if (!file.exists()) if (!file.exists())
return false; return false;
try { try {
List<ManifestEntry> manifestEntries = CommonLogic.getAssetJson(context, "apk_files_manifest.json", new TypeToken<List<ManifestEntry>>() { ApkFilesManifest apkFilesManifest = CommonLogic.getAssetJson(context, "apk_files_manifest.json", ApkFilesManifest.class);
}.getType()); if (apkFilesManifest == null)
if (manifestEntries == null)
return false; return false;
List<ManifestEntry> manifestEntries = apkFilesManifest.getManifestEntries();
List<ZipEntrySource> zipEntrySourceList = new ArrayList<>(); List<ZipEntrySource> zipEntrySourceList = new ArrayList<>();
for (ManifestEntry entry : manifestEntries) {
zipEntrySourceList.add(new ByteSource(entry.getTargetPath(), CommonLogic.getAssetBytes(context, entry.getAssetPath()), entry.getCompression()));
}
byte[] manifest = ZipUtil.unpackEntry(file, "AndroidManifest.xml"); byte[] manifest = ZipUtil.unpackEntry(file, "AndroidManifest.xml");
byte[] modifiedManifest = modifyManifest(manifest); List<ApkFilesManifest> apkFilesManifests = Lists.newArrayList(apkFilesManifest);
byte[] modifiedManifest = modifyManifest(manifest, apkFilesManifests);
if(apkFilesManifests.size() == 0) {
errorMessage.set(context.getString(R.string.error_no_supported_game_version));
return false;
}
if(modifiedManifest == null) { if(modifiedManifest == null) {
errorMessage.set(context.getString(R.string.failed_to_process_manifest));
return false; return false;
} }
zipEntrySourceList.add(new ByteSource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED)); zipEntrySourceList.add(new ByteSource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
for (ManifestEntry entry : manifestEntries) {
zipEntrySourceList.add(new ByteSource(entry.getTargetPath(), CommonLogic.getAssetBytes(context, entry.getAssetPath()), entry.getCompression()));
}
ZipUtil.addOrReplaceEntries(file, zipEntrySourceList.toArray(new ZipEntrySource[0])); ZipUtil.addOrReplaceEntries(file, zipEntrySourceList.toArray(new ZipEntrySource[0]));
return true; return true;
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Patch error", e); Log.e(TAG, "Patch error", e);
errorMessage.set(e.getLocalizedMessage());
} }
return false; return false;
} }
private byte[] modifyManifest(byte[] bytes) { private byte[] modifyManifest(byte[] bytes, List<ApkFilesManifest> manifests) {
AtomicReference<String> packageName = new AtomicReference<>(); AtomicReference<String> packageName = new AtomicReference<>();
Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> { Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> {
if (attr.type == NodeVisitor.TYPE_STRING) { if (attr.type == NodeVisitor.TYPE_STRING) {
@ -140,9 +156,32 @@ public class ApkPatcher {
break; break;
} }
} }
else if(attr.type == NodeVisitor.TYPE_FIRST_INT) {
if(StringUtils.equals(attr.name, "versionCode")){
long versionCode = (int) attr.obj;
Iterables.removeIf(manifests, manifest -> {
if (manifest.getMinBuildCode() != null) {
if (versionCode < manifest.getMinBuildCode()) {
return true;
}
}
if (manifest.getMaxBuildCode() != null) {
if (versionCode > manifest.getMinBuildCode()) {
return true;
}
}
return false;
});
}
}
return true; return true;
}; };
return CommonLogic.modifyManifest(bytes, processLogic); try {
return CommonLogic.modifyManifest(bytes, processLogic);
}catch (Exception e) {
errorMessage.set(e.getLocalizedMessage());
return null;
}
} }
public String sign(String apkPath) { public String sign(String apkPath) {
@ -156,16 +195,14 @@ public class ApkPatcher {
} }
String alias = ks.aliases().nextElement(); String alias = ks.aliases().nextElement();
X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias); X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias);
try { PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray()); ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", apkPath, signApkPath);
ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", apkPath, signApkPath); new File(apkPath).delete();
new File(apkPath).delete(); return signApkPath;
return signApkPath;
} catch (NoSuchAlgorithmException ignored) {
}
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Sign error", e); Log.e(TAG, "Sign error", e);
errorMessage.set(e.getLocalizedMessage());
} }
return null; return null;
} }
@ -179,6 +216,7 @@ public class ApkPatcher {
context.startActivity(intent); context.startActivity(intent);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
Log.e(TAG, "Install error", e); Log.e(TAG, "Install error", e);
errorMessage.set(e.getLocalizedMessage());
} }
} }
@ -196,4 +234,7 @@ public class ApkPatcher {
return Uri.fromFile(file); return Uri.fromFile(file);
} }
public AtomicReference<String> getErrorMessage() {
return errorMessage;
}
} }

View File

@ -59,6 +59,17 @@ public class CommonLogic {
return null; return null;
} }
public static <T> T getAssetJson(Context context, String filename, Class<T> tClass) {
try {
InputStream inputStream = context.getAssets().open(filename);
try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
return new Gson().fromJson(CharStreams.toString(reader), tClass);
}
} catch (IOException ignored) {
}
return null;
}
public static <T> T getAssetJson(Context context, String filename, Type type) { public static <T> T getAssetJson(Context context, String filename, Type type) {
try { try {
InputStream inputStream = context.getAssets().open(filename); InputStream inputStream = context.getAssets().open(filename);
@ -177,25 +188,16 @@ public class CommonLogic {
context.startActivity(intent); context.startActivity(intent);
} }
public static byte[] modifyManifest(byte[] bytes, Predicate<ManifestTagVisitor.AttrArgs> processLogic) { public static byte[] modifyManifest(byte[] bytes, Predicate<ManifestTagVisitor.AttrArgs> processLogic) throws IOException {
AxmlReader reader = new AxmlReader(bytes); AxmlReader reader = new AxmlReader(bytes);
AxmlWriter writer = new AxmlWriter(); AxmlWriter writer = new AxmlWriter();
try { reader.accept(new AxmlVisitor(writer) {
reader.accept(new AxmlVisitor(writer) { @Override
@Override public NodeVisitor child(String ns, String name) {
public NodeVisitor child(String ns, String name) { NodeVisitor child = super.child(ns, name);
NodeVisitor child = super.child(ns, name); return new ManifestTagVisitor(child, processLogic);
return new ManifestTagVisitor(child, processLogic); }
} });
}); return writer.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
try {
return writer.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
} }
} }

View File

@ -26,8 +26,11 @@ public class GameLauncher {
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair); CommonLogic.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair);
return; return;
} }
Intent intent = packageManager.getLaunchIntentForPackage(Constants.TARGET_PACKAGE_NAME); ModAssetsManager modAssetsManager = new ModAssetsManager(root);
context.startActivity(intent); if(modAssetsManager.checkModEnvironment()) {
Intent intent = packageManager.getLaunchIntentForPackage(Constants.TARGET_PACKAGE_NAME);
context.startActivity(intent);
}
} catch (PackageManager.NameNotFoundException ignored) { } catch (PackageManager.NameNotFoundException ignored) {
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_smapi_not_installed); CommonLogic.showAlertDialog(root, R.string.error, R.string.error_smapi_not_installed);
} }

View File

@ -16,6 +16,8 @@ import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants; import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.entity.ModManifestEntry; import com.zane.smapiinstaller.entity.ModManifestEntry;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.NameMapper;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
import java.io.File; import java.io.File;
@ -23,6 +25,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
public class ModAssetsManager { public class ModAssetsManager {
@ -43,7 +46,7 @@ public class ModAssetsManager {
if(currentFile != null && currentFile.exists()) { if(currentFile != null && currentFile.exists()) {
boolean foundManifest = false; boolean foundManifest = false;
for(File file : currentFile.listFiles(File::isFile)) { for(File file : currentFile.listFiles(File::isFile)) {
if(file.getName().equalsIgnoreCase("manifest.json")) { if(StringUtils.equalsIgnoreCase(file.getName(), "manifest.json")) {
ModManifestEntry manifest = CommonLogic.getFileJson(file, new TypeToken<ModManifestEntry>(){}.getType()); ModManifestEntry manifest = CommonLogic.getFileJson(file, new TypeToken<ModManifestEntry>(){}.getType());
foundManifest = true; foundManifest = true;
if(manifest != null) { if(manifest != null) {
@ -70,7 +73,7 @@ public class ModAssetsManager {
File modFolder = new File(Environment.getExternalStorageDirectory(), Constants.MOD_PATH); File modFolder = new File(Environment.getExternalStorageDirectory(), Constants.MOD_PATH);
ImmutableListMultimap<String, ModManifestEntry> installedModMap = Multimaps.index(findAllInstalledMods(), ModManifestEntry::getUniqueID); ImmutableListMultimap<String, ModManifestEntry> installedModMap = Multimaps.index(findAllInstalledMods(), ModManifestEntry::getUniqueID);
for (ModManifestEntry mod : modManifestEntries) { for (ModManifestEntry mod : modManifestEntries) {
if(installedModMap.containsKey(mod.getUniqueID())) { if(installedModMap.containsKey(mod.getUniqueID()) || installedModMap.containsKey(mod.getUniqueID().replace("ZaneYork.CustomLocalization", "SMAPI.CustomLocalization"))) {
ImmutableList<ModManifestEntry> installedMods = installedModMap.get(mod.getUniqueID()); ImmutableList<ModManifestEntry> installedMods = installedModMap.get(mod.getUniqueID());
if(installedMods.size() > 1) { if(installedMods.size() > 1) {
CommonLogic.showAlertDialog(root, R.string.error, CommonLogic.showAlertDialog(root, R.string.error,
@ -79,7 +82,7 @@ public class ModAssetsManager {
return false; return false;
} }
try { try {
ZipUtil.unpackEntry(context.getAssets().open(mod.getAssetPath()), mod.getName(), new File(installedMods.get(0).getAssetPath())); ZipUtil.unpack(context.getAssets().open(mod.getAssetPath()), new File(installedMods.get(0).getAssetPath()), (name)-> StringUtils.removeStart(name, mod.getName() + "/"));
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Install Mod Error", e); Log.e(TAG, "Install Mod Error", e);
} }
@ -94,4 +97,8 @@ public class ModAssetsManager {
} }
return true; return true;
} }
public boolean checkModEnvironment() {
return true;
}
} }

View File

@ -61,7 +61,7 @@ public class ConfigEditFragment extends Fragment {
} }
} }
catch (Exception e) { catch (Exception e) {
CommonLogic.showAlertDialog(getView(), R.string.error, e.getMessage()); CommonLogic.showAlertDialog(getView(), R.string.error, e.getLocalizedMessage());
} }
} }

View File

@ -17,7 +17,6 @@ import com.zane.smapiinstaller.R;
public class ConfigFragment extends Fragment { public class ConfigFragment extends Fragment {
private ConfigViewModel configViewModel;
@BindView(R.id.view_mod_list) @BindView(R.id.view_mod_list)
RecyclerView recyclerView; RecyclerView recyclerView;
@ -26,13 +25,11 @@ public class ConfigFragment extends Fragment {
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_config, container, false); View root = inflater.inflate(R.layout.fragment_config, container, false);
ButterKnife.bind(this, root); ButterKnife.bind(this, root);
configViewModel = new ConfigViewModel(root);
recyclerView.setLayoutManager(new LinearLayoutManager(this.getContext())); recyclerView.setLayoutManager(new LinearLayoutManager(this.getContext()));
recyclerView.setAdapter(new ModManifestAdapter(configViewModel.getModList().getValue())); ConfigViewModel configViewModel = new ConfigViewModel(root);
recyclerView.addItemDecoration(new DividerItemDecoration(this.getContext(), DividerItemDecoration.VERTICAL)); ModManifestAdapter modManifestAdapter = new ModManifestAdapter(configViewModel);
configViewModel.getModList().observe(getViewLifecycleOwner(), modList -> { recyclerView.setAdapter(modManifestAdapter);
recyclerView.getAdapter().notifyDataSetChanged(); recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
});
return root; return root;
} }
} }

View File

@ -1,29 +1,55 @@
package com.zane.smapiinstaller.ui.config; package com.zane.smapiinstaller.ui.config;
import android.os.FileObserver;
import android.view.View; import android.view.View;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.zane.smapiinstaller.entity.ModManifestEntry; import com.zane.smapiinstaller.entity.ModManifestEntry;
import com.zane.smapiinstaller.logic.ModAssetsManager; import com.zane.smapiinstaller.logic.ModAssetsManager;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import androidx.recyclerview.widget.RecyclerView;
class ConfigViewModel extends ViewModel { class ConfigViewModel extends ViewModel {
private MutableLiveData<List<ModManifestEntry>> modList; @NonNull
private List<ModManifestEntry> modList;
public ConfigViewModel(View root) { private RecyclerView view;
ConfigViewModel(View root) {
ModAssetsManager manager = new ModAssetsManager(root); ModAssetsManager manager = new ModAssetsManager(root);
this.modList = new MutableLiveData<>(); this.modList = manager.findAllInstalledMods();
List<ModManifestEntry> entryList = manager.findAllInstalledMods(); Collections.sort(this.modList, (a, b)-> a.getName().compareTo(b.getName()));
Collections.sort(entryList, (a, b)-> a.getName().compareTo(b.getName()));
this.modList.setValue(entryList);
} }
public MutableLiveData<List<ModManifestEntry>> getModList() { @NonNull
public List<ModManifestEntry> getModList() {
return modList; return modList;
} }
public List<Integer> removeAll(Predicate<ModManifestEntry> predicate) {
List<Integer> deletedId = new ArrayList<>();
for (int i = modList.size() - 1; i >=0 ; i--) {
if(predicate.apply(modList.get(i))) {
modList.remove(i);
deletedId.add(i);
}
}
return deletedId;
}
} }

View File

@ -7,11 +7,16 @@ import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.DialogAction;
import com.google.common.collect.Iterables;
import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.entity.ModManifestEntry; import com.zane.smapiinstaller.entity.ModManifestEntry;
import com.zane.smapiinstaller.logic.CommonLogic; import com.zane.smapiinstaller.logic.CommonLogic;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.List; import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -23,21 +28,22 @@ import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.ViewHolder> { public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.ViewHolder> {
private List<ModManifestEntry> modList; private ConfigViewModel model;
public ModManifestAdapter(List<ModManifestEntry> modList){ public ModManifestAdapter(ConfigViewModel model){
this.modList=modList; this.model=model;
} }
@NonNull @NonNull
@Override @Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view= LayoutInflater.from(parent.getContext()).inflate(R.layout.mod_list_item,null); View view= LayoutInflater.from(parent.getContext()).inflate(R.layout.mod_list_item,parent, false);
return new ViewHolder(view); return new ViewHolder(view);
} }
@Override @Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ModManifestEntry mod = modList.get(position); ModManifestEntry mod = model.getModList().get(position);
holder.modName.setText(mod.getName()); holder.modName.setText(mod.getName());
holder.modDescription.setText(mod.getDescription()); holder.modDescription.setText(mod.getDescription());
holder.setModPath(mod.getAssetPath()); holder.setModPath(mod.getAssetPath());
@ -45,10 +51,10 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
@Override @Override
public int getItemCount() { public int getItemCount() {
return modList.size(); return model.getModList().size();
} }
static class ViewHolder extends RecyclerView.ViewHolder{ class ViewHolder extends RecyclerView.ViewHolder{
private String modPath; private String modPath;
void setModPath(String modPath) { void setModPath(String modPath) {
this.modPath = modPath; this.modPath = modPath;
@ -75,7 +81,15 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
if (which == DialogAction.POSITIVE) { if (which == DialogAction.POSITIVE) {
File file = new File(modPath); File file = new File(modPath);
if (file.exists()) { if (file.exists()) {
file.delete(); try {
FileUtils.forceDelete(file);
List<Integer> removed = model.removeAll(entry -> StringUtils.equals(entry.getAssetPath(), modPath));
for (int idx : removed) {
notifyItemRemoved(idx);
}
} catch (IOException e) {
CommonLogic.showAlertDialog(itemView, R.string.error, e.getMessage());
}
} }
} }
}); });

View File

@ -0,0 +1,69 @@
package com.zane.smapiinstaller.ui.download;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.entity.DownloadableContent;
import java.util.List;
/**
* {@link RecyclerView.Adapter} that can display a {@link DownloadableContent}
*/
public class DownloadableContentAdapter extends RecyclerView.Adapter<DownloadableContentAdapter.ViewHolder> {
private final List<DownloadableContent> downloadableContentList;
public DownloadableContentAdapter(List<DownloadableContent> items) {
downloadableContentList = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.download_content_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.downloadableContent = downloadableContentList.get(position);
holder.typeTextView.setText(holder.downloadableContent.getType());
holder.nameTextView.setText(holder.downloadableContent.getName());
holder.descriptionTextView.setText(holder.downloadableContent.getDescription());
}
@Override
public int getItemCount() {
return downloadableContentList.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.text_item_type)
TextView typeTextView;
@BindView(R.id.text_item_name)
TextView nameTextView;
@BindView(R.id.text_item_description)
TextView descriptionTextView;
public DownloadableContent downloadableContent;
public ViewHolder(View view) {
super(view);
ButterKnife.bind(this, itemView);
}
@OnClick(R.id.button_remove_content) void removeContent() {
}
@OnClick(R.id.button_download_content) void downloadContent() {
}
}
}

View File

@ -0,0 +1,47 @@
package com.zane.smapiinstaller.ui.download;
import android.content.Context;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.entity.DownloadableContentList;
import com.zane.smapiinstaller.logic.CommonLogic;
/**
* A fragment representing a list of Items.
*/
public class DownloadableContentFragment extends Fragment {
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public DownloadableContentFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_download_content_list, container, false);
// Set the adapter
if (view instanceof RecyclerView) {
Context context = view.getContext();
RecyclerView recyclerView = (RecyclerView) view;
recyclerView.setLayoutManager(new LinearLayoutManager(context));
DownloadableContentList contentList = CommonLogic.getAssetJson(context, "downloadable_content_list.json", DownloadableContentList.class);
recyclerView.setAdapter(new DownloadableContentAdapter(contentList.getContents()));
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
}
return view;
}
}

View File

@ -13,6 +13,8 @@ import com.zane.smapiinstaller.logic.ApkPatcher;
import com.zane.smapiinstaller.logic.CommonLogic; import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ModAssetsManager; import com.zane.smapiinstaller.logic.ModAssetsManager;
import org.apache.commons.lang3.StringUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import butterknife.ButterKnife; import butterknife.ButterKnife;
@ -52,12 +54,12 @@ public class InstallFragment extends Fragment {
CommonLogic.setProgressDialogState(root, dialog, R.string.extracting_package, 0); CommonLogic.setProgressDialogState(root, dialog, R.string.extracting_package, 0);
String path = patcher.extract(); String path = patcher.extract();
if (path == null) { if (path == null) {
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_game_not_found); CommonLogic.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found)));
return; return;
} }
CommonLogic.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 10); CommonLogic.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 10);
if (!CommonLogic.unpackSmapiFiles(context, path, false)) { if (!CommonLogic.unpackSmapiFiles(context, path, false)) {
CommonLogic.showAlertDialog(root, R.string.error, R.string.failed_to_unpack_smapi_files); CommonLogic.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_unpack_smapi_files)));
return; return;
} }
ModAssetsManager modAssetsManager = new ModAssetsManager(root); ModAssetsManager modAssetsManager = new ModAssetsManager(root);
@ -65,13 +67,13 @@ public class InstallFragment extends Fragment {
modAssetsManager.installDefaultMods(); modAssetsManager.installDefaultMods();
CommonLogic.setProgressDialogState(root, dialog, R.string.patching_package, 25); CommonLogic.setProgressDialogState(root, dialog, R.string.patching_package, 25);
if (!patcher.patch(path)) { if (!patcher.patch(path)) {
CommonLogic.showAlertDialog(root, R.string.error, R.string.failed_to_patch_game); CommonLogic.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)));
return; return;
} }
CommonLogic.setProgressDialogState(root, dialog, R.string.signing_package, 55); CommonLogic.setProgressDialogState(root, dialog, R.string.signing_package, 55);
String signPath = patcher.sign(path); String signPath = patcher.sign(path);
if (signPath == null) { if (signPath == null) {
CommonLogic.showAlertDialog(root, R.string.error, R.string.failed_to_sign_game); CommonLogic.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return; return;
} }
CommonLogic.setProgressDialogState(root, dialog, R.string.installing_package, 99); CommonLogic.setProgressDialogState(root, dialog, R.string.installing_package, 99);

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#eee"/>
<size android:width="1dp"/>
</shape>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginEnd="40dp"
android:divider="@drawable/horizontal_divider"
android:showDividers="middle"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/vertical_divider"
android:showDividers="middle"
android:orientation="horizontal">
<TextView
android:id="@+id/text_item_type"
android:textSize="20sp"
android:paddingRight="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/text_item_name"
android:textSize="20sp"
android:paddingLeft="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/text_item_description"
android:textSize="16sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:orientation="vertical">
<Button
android:id="@+id/button_remove_content"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@android:drawable/ic_menu_delete" />
<Button
android:id="@+id/button_download_content"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@android:drawable/ic_menu_add" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:name="com.zane.smapiinstaller.ui.download.DownloadContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".ui.download.DownloadableContentFragment"
tools:listitem="@layout/download_content_item" />

View File

@ -3,13 +3,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginEnd="80dp" android:layout_marginEnd="40dp"
android:divider="@drawable/divider" android:divider="@drawable/horizontal_divider"
android:showDividers="middle" android:showDividers="middle"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -30,7 +30,8 @@
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"> android:layout_alignParentEnd="true"
android:orientation="vertical">
<Button <Button
android:id="@+id/button_remove_mod" android:id="@+id/button_remove_mod"
android:layout_width="32dp" android:layout_width="32dp"

View File

@ -12,6 +12,10 @@
android:id="@+id/nav_config" android:id="@+id/nav_config"
android:icon="@android:drawable/ic_menu_edit" android:icon="@android:drawable/ic_menu_edit"
android:title="@string/menu_config" /> android:title="@string/menu_config" />
<item
android:id="@+id/nav_download"
android:icon="@android:drawable/ic_dialog_dialer"
android:title="@string/menu_download" />
<item <item
android:id="@+id/nav_help" android:id="@+id/nav_help"
android:icon="@android:drawable/ic_menu_help" android:icon="@android:drawable/ic_menu_help"

View File

@ -39,4 +39,9 @@
<argument android:name="editable" android:defaultValue="true" app:argType="boolean"/> <argument android:name="editable" android:defaultValue="true" app:argType="boolean"/>
</action> </action>
</fragment> </fragment>
<fragment
android:id="@+id/nav_download"
android:name="com.zane.smapiinstaller.ui.download.DownloadableContentFragment"
android:label="@string/menu_download"
tools:layout="@layout/fragment_download_content_list" />
</navigation> </navigation>

View File

@ -40,4 +40,8 @@
<string name="test_message">暂处于内测阶段, Q群860453392</string> <string name="test_message">暂处于内测阶段, Q群860453392</string>
<string name="button_logs">日志</string> <string name="button_logs">日志</string>
<string name="smapi_game_name">SMAPI星露谷物语</string> <string name="smapi_game_name">SMAPI星露谷物语</string>
<string name="error_failed_to_create_file">无法创建以下文件: %s</string>
<string name="failed_to_process_manifest">无法处理AndroidManifest.xml文件</string>
<string name="error_no_supported_game_version">游戏版本不支持,请更新版本或者下载兼容包</string>
<string name="menu_download">下载</string>
</resources> </resources>

View File

@ -5,4 +5,5 @@
<dimen name="nav_header_vertical_spacing">8dp</dimen> <dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen> <dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen> <dimen name="fab_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources> </resources>

View File

@ -40,4 +40,8 @@
<string name="test_message">Still at test stage now, not release yet.</string> <string name="test_message">Still at test stage now, not release yet.</string>
<string name="button_logs">Logs</string> <string name="button_logs">Logs</string>
<string name="smapi_game_name">SMAPI Stardew Valley</string> <string name="smapi_game_name">SMAPI Stardew Valley</string>
<string name="error_failed_to_create_file">Failed to create target file : %s</string>
<string name="failed_to_process_manifest">Failed to process AndroidManifest.xml file</string>
<string name="error_no_supported_game_version">Game version not supported, upgrade or download compat package first</string>
<string name="menu_download">Download</string>
</resources> </resources>