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'
// https://mvnrepository.com/artifact/com.jakewharton/butterknife
implementation 'com.jakewharton:butterknife:10.2.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
// https://mvnrepository.com/artifact/com.google.guava/guava
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'
// 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'
implementation 'com.lzy.net:okgo:3.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
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 ----------
#okhttp
-dontwarn okhttp3.**
-keep class okhttp3.**{*;}
#okio
-dontwarn okio.**
-keep class okio.**{*;}
-keep class com.zane.** { *; }
-keep class pxb.android.** { *; }

View File

@ -2,12 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

View File

@ -1,62 +1,67 @@
[
{
"targetPath": "classes.dex",
"assetPath": "apk/classes.dex",
"compression": 8
},
{
"targetPath": "res/mipmap-mdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-hdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-xhdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-xxhdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-xxxhdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "assemblies/Newtonsoft.Json.dll",
"assetPath": "apk/Newtonsoft.Json.dll",
"compression": 0
},
{
"targetPath": "assemblies/SMAPI.Toolkit.CoreInterfaces.dll",
"assetPath": "apk/SMAPI.Toolkit.CoreInterfaces.dll",
"compression": 0
},
{
"targetPath": "assemblies/SMAPI.Toolkit.dll",
"assetPath": "apk/SMAPI.Toolkit.dll",
"compression": 0
},
{
"targetPath": "assemblies/StardewModdingAPI.dll",
"assetPath": "apk/StardewModdingAPI.dll",
"compression": 0
},
{
"targetPath": "assemblies/System.Data.dll",
"assetPath": "apk/System.Data.dll",
"compression": 0
},
{
"targetPath": "assemblies/System.Numerics.dll",
"assetPath": "apk/System.Numerics.dll",
"compression": 0
}
]
{
"minBuildCode": 138,
"maxBuildCode": null,
"withMonoMod": true,
"manifestEntries": [
{
"targetPath": "classes.dex",
"assetPath": "apk/classes.dex",
"compression": 8
},
{
"targetPath": "res/mipmap-mdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-hdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-xhdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-xxhdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "res/mipmap-xxxhdpi-v4/ic_launcher_foreground.png",
"assetPath": "apk/ic_launcher_foreground.png",
"compression": 8
},
{
"targetPath": "assemblies/Newtonsoft.Json.dll",
"assetPath": "apk/Newtonsoft.Json.dll",
"compression": 0
},
{
"targetPath": "assemblies/SMAPI.Toolkit.CoreInterfaces.dll",
"assetPath": "apk/SMAPI.Toolkit.CoreInterfaces.dll",
"compression": 0
},
{
"targetPath": "assemblies/SMAPI.Toolkit.dll",
"assetPath": "apk/SMAPI.Toolkit.dll",
"compression": 0
},
{
"targetPath": "assemblies/StardewModdingAPI.dll",
"assetPath": "apk/StardewModdingAPI.dll",
"compression": 0
},
{
"targetPath": "assemblies/System.Data.dll",
"assetPath": "apk/System.Data.dll",
"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",
"Name": "VirtualKeyboard",
"UniqueID": "SMAPI.VirtualKeyboard"
"UniqueID": "VirtualKeyboard"
},
{
"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;
import lombok.Data;
@Data
public class ManifestEntry {
private String targetPath;
private String assetPath;
private int compression;
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;
import lombok.Data;
@Data
public class ModManifestEntry {
private String assetPath;
private String Name;
private String UniqueID;
private String Description;
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 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.gson.reflect.TypeToken;
import com.zane.smapiinstaller.BuildConfig;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.entity.ApkFilesManifest;
import com.zane.smapiinstaller.entity.ManifestEntry;
import net.fornwall.apksigner.KeyStoreFileManager;
import net.fornwall.apksigner.ZipSigner;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.ByteSource;
import org.zeroturnaround.zip.ZipEntrySource;
import org.zeroturnaround.zip.ZipUtil;
@ -29,7 +33,6 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@ -48,6 +51,8 @@ public class ApkPatcher {
private static final String TAG = "PATCHER";
private AtomicReference<String> errorMessage = new AtomicReference<>();
public ApkPatcher(Context context) {
this.context = context;
}
@ -56,8 +61,10 @@ public class ApkPatcher {
PackageManager packageManager = context.getPackageManager();
List<String> packageNames = CommonLogic.getAssetJson(context, "package_names.json", new TypeToken<List<String>>() {
}.getType());
if (packageNames == null)
if (packageNames == null) {
errorMessage.set(context.getString(R.string.error_game_not_found));
return null;
}
for (String packageName : packageNames) {
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
@ -69,6 +76,7 @@ public class ApkPatcher {
File dest = new File(externalFilesDir.getAbsolutePath() + "/SMAPI Installer/");
if (!dest.exists()) {
if (!dest.mkdir()) {
errorMessage.set(String.format(context.getString(R.string.error_failed_to_create_file), dest.getAbsolutePath()));
return null;
}
}
@ -78,6 +86,7 @@ public class ApkPatcher {
}
} catch (PackageManager.NameNotFoundException | IOException e) {
Log.e(TAG, "Extract error", e);
errorMessage.set(e.getLocalizedMessage());
}
}
return null;
@ -90,29 +99,36 @@ public class ApkPatcher {
if (!file.exists())
return false;
try {
List<ManifestEntry> manifestEntries = CommonLogic.getAssetJson(context, "apk_files_manifest.json", new TypeToken<List<ManifestEntry>>() {
}.getType());
if (manifestEntries == null)
ApkFilesManifest apkFilesManifest = CommonLogic.getAssetJson(context, "apk_files_manifest.json", ApkFilesManifest.class);
if (apkFilesManifest == null)
return false;
List<ManifestEntry> manifestEntries = apkFilesManifest.getManifestEntries();
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[] 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) {
errorMessage.set(context.getString(R.string.failed_to_process_manifest));
return false;
}
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]));
return true;
} catch (Exception e) {
Log.e(TAG, "Patch error", e);
errorMessage.set(e.getLocalizedMessage());
}
return false;
}
private byte[] modifyManifest(byte[] bytes) {
private byte[] modifyManifest(byte[] bytes, List<ApkFilesManifest> manifests) {
AtomicReference<String> packageName = new AtomicReference<>();
Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> {
if (attr.type == NodeVisitor.TYPE_STRING) {
@ -140,9 +156,32 @@ public class ApkPatcher {
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 CommonLogic.modifyManifest(bytes, processLogic);
try {
return CommonLogic.modifyManifest(bytes, processLogic);
}catch (Exception e) {
errorMessage.set(e.getLocalizedMessage());
return null;
}
}
public String sign(String apkPath) {
@ -156,16 +195,14 @@ public class ApkPatcher {
}
String alias = ks.aliases().nextElement();
X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias);
try {
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", apkPath, signApkPath);
new File(apkPath).delete();
return signApkPath;
} catch (NoSuchAlgorithmException ignored) {
}
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", apkPath, signApkPath);
new File(apkPath).delete();
return signApkPath;
}
} catch (Exception e) {
Log.e(TAG, "Sign error", e);
errorMessage.set(e.getLocalizedMessage());
}
return null;
}
@ -179,6 +216,7 @@ public class ApkPatcher {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Install error", e);
errorMessage.set(e.getLocalizedMessage());
}
}
@ -196,4 +234,7 @@ public class ApkPatcher {
return Uri.fromFile(file);
}
public AtomicReference<String> getErrorMessage() {
return errorMessage;
}
}

View File

@ -59,6 +59,17 @@ public class CommonLogic {
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) {
try {
InputStream inputStream = context.getAssets().open(filename);
@ -177,25 +188,16 @@ public class CommonLogic {
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);
AxmlWriter writer = new AxmlWriter();
try {
reader.accept(new AxmlVisitor(writer) {
@Override
public NodeVisitor child(String ns, String name) {
NodeVisitor child = super.child(ns, name);
return new ManifestTagVisitor(child, processLogic);
}
});
} catch (IOException e) {
e.printStackTrace();
}
try {
return writer.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
reader.accept(new AxmlVisitor(writer) {
@Override
public NodeVisitor child(String ns, String name) {
NodeVisitor child = super.child(ns, name);
return new ManifestTagVisitor(child, processLogic);
}
});
return writer.toByteArray();
}
}

View File

@ -26,8 +26,11 @@ public class GameLauncher {
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair);
return;
}
Intent intent = packageManager.getLaunchIntentForPackage(Constants.TARGET_PACKAGE_NAME);
context.startActivity(intent);
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
if(modAssetsManager.checkModEnvironment()) {
Intent intent = packageManager.getLaunchIntentForPackage(Constants.TARGET_PACKAGE_NAME);
context.startActivity(intent);
}
} catch (PackageManager.NameNotFoundException ignored) {
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.entity.ModManifestEntry;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.NameMapper;
import org.zeroturnaround.zip.ZipUtil;
import java.io.File;
@ -23,6 +25,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
public class ModAssetsManager {
@ -43,7 +46,7 @@ public class ModAssetsManager {
if(currentFile != null && currentFile.exists()) {
boolean foundManifest = false;
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());
foundManifest = true;
if(manifest != null) {
@ -70,7 +73,7 @@ public class ModAssetsManager {
File modFolder = new File(Environment.getExternalStorageDirectory(), Constants.MOD_PATH);
ImmutableListMultimap<String, ModManifestEntry> installedModMap = Multimaps.index(findAllInstalledMods(), ModManifestEntry::getUniqueID);
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());
if(installedMods.size() > 1) {
CommonLogic.showAlertDialog(root, R.string.error,
@ -79,7 +82,7 @@ public class ModAssetsManager {
return false;
}
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) {
Log.e(TAG, "Install Mod Error", e);
}
@ -94,4 +97,8 @@ public class ModAssetsManager {
}
return true;
}
public boolean checkModEnvironment() {
return true;
}
}

View File

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

View File

@ -1,29 +1,55 @@
package com.zane.smapiinstaller.ui.config;
import android.os.FileObserver;
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.logic.ModAssetsManager;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.recyclerview.widget.RecyclerView;
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);
this.modList = new MutableLiveData<>();
List<ModManifestEntry> entryList = manager.findAllInstalledMods();
Collections.sort(entryList, (a, b)-> a.getName().compareTo(b.getName()));
this.modList.setValue(entryList);
this.modList = manager.findAllInstalledMods();
Collections.sort(this.modList, (a, b)-> a.getName().compareTo(b.getName()));
}
public MutableLiveData<List<ModManifestEntry>> getModList() {
@NonNull
public List<ModManifestEntry> getModList() {
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 com.afollestad.materialdialogs.DialogAction;
import com.google.common.collect.Iterables;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.entity.ModManifestEntry;
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.IOException;
import java.util.List;
import androidx.annotation.NonNull;
@ -23,21 +28,22 @@ import butterknife.ButterKnife;
import butterknife.OnClick;
public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.ViewHolder> {
private List<ModManifestEntry> modList;
private ConfigViewModel model;
public ModManifestAdapter(List<ModManifestEntry> modList){
this.modList=modList;
public ModManifestAdapter(ConfigViewModel model){
this.model=model;
}
@NonNull
@Override
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);
}
@Override
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.modDescription.setText(mod.getDescription());
holder.setModPath(mod.getAssetPath());
@ -45,10 +51,10 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
@Override
public int getItemCount() {
return modList.size();
return model.getModList().size();
}
static class ViewHolder extends RecyclerView.ViewHolder{
class ViewHolder extends RecyclerView.ViewHolder{
private String modPath;
void setModPath(String modPath) {
this.modPath = modPath;
@ -75,7 +81,15 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
if (which == DialogAction.POSITIVE) {
File file = new File(modPath);
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.ModAssetsManager;
import org.apache.commons.lang3.StringUtils;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import butterknife.ButterKnife;
@ -52,12 +54,12 @@ public class InstallFragment extends Fragment {
CommonLogic.setProgressDialogState(root, dialog, R.string.extracting_package, 0);
String path = patcher.extract();
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;
}
CommonLogic.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 10);
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;
}
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
@ -65,13 +67,13 @@ public class InstallFragment extends Fragment {
modAssetsManager.installDefaultMods();
CommonLogic.setProgressDialogState(root, dialog, R.string.patching_package, 25);
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;
}
CommonLogic.setProgressDialogState(root, dialog, R.string.signing_package, 55);
String signPath = patcher.sign(path);
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;
}
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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="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="80dp"
android:divider="@drawable/divider"
android:layout_marginEnd="40dp"
android:divider="@drawable/horizontal_divider"
android:showDividers="middle"
android:orientation="vertical">
<TextView
@ -30,7 +30,8 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true">
android:layout_alignParentEnd="true"
android:orientation="vertical">
<Button
android:id="@+id/button_remove_mod"
android:layout_width="32dp"

View File

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

View File

@ -39,4 +39,9 @@
<argument android:name="editable" android:defaultValue="true" app:argType="boolean"/>
</action>
</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>

View File

@ -40,4 +40,8 @@
<string name="test_message">暂处于内测阶段, Q群860453392</string>
<string name="button_logs">日志</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>

View File

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

View File

@ -40,4 +40,8 @@
<string name="test_message">Still at test stage now, not release yet.</string>
<string name="button_logs">Logs</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>