1.Mod update check(in progress)

2.Upgrade material dialog dependency to latest version
3.Virtual keyboard update(auto hidden feature)
This commit is contained in:
ZaneYork 2020-03-31 11:22:24 +08:00
parent e69565d5e3
commit f15d305661
29 changed files with 524 additions and 227 deletions

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AlibabaAvoidManuallyCreateThread" enabled="false" level="CRITICAL" enabled_by_default="false" />
</profile>
</component>

View File

@ -1,6 +1,8 @@
apply plugin: 'com.android.application'
apply plugin: "androidx.navigation.safeargs"
apply plugin: 'org.greenrobot.greendao'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
@ -57,14 +59,18 @@ dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment:2.3.0-alpha04'
implementation 'androidx.navigation:navigation-ui:2.3.0-alpha04'
def nav_version = "2.3.0-alpha04"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.madgag.spongycastle:core:1.54.0.0'
implementation 'com.madgag.spongycastle:prov:1.54.0.0'
implementation 'com.madgag.spongycastle:pkix:1.54.0.0'
implementation 'com.madgag.spongycastle:pg:1.54.0.0'
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:input:3.3.0'
implementation 'com.afollestad.material-dialogs:lifecycle:3.3.0'
implementation 'com.lmntrx.android.library.livin.missme:missme:0.1.5'
// https://mvnrepository.com/artifact/com.jakewharton/butterknife
implementation 'com.jakewharton:butterknife:10.2.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

View File

@ -1,5 +1,5 @@
{
"version": 8,
"version": 9,
"contents": [
{
"type": "COMPAT",
@ -14,8 +14,8 @@
"name": "SMAPI for Galaxy Store",
"assetPath": "compat/samsung_138/",
"description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.4.1.1",
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138_3.zip",
"hash": "d559c4b62fbf598eb8fff99ddcd152d7e34f6e30aa32a64d728ce1dd174b0dff"
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138_4.zip",
"hash": "e6d018c164d9f7d482f3d28dddf33dd5d7463bc414b61af9d65e3ca0b1eecc93"
},
{
"type": "LOCALE",

View File

@ -1,5 +1,5 @@
{
"version": 8,
"version": 9,
"contents": [
{
"type": "COMPAT",
@ -14,8 +14,8 @@
"name": "SMAPI for Galaxy Store",
"assetPath": "compat/samsung_138/",
"description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.4.1.1",
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138_3.zip",
"hash": "d559c4b62fbf598eb8fff99ddcd152d7e34f6e30aa32a64d728ce1dd174b0dff"
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138_4.zip",
"hash": "e6d018c164d9f7d482f3d28dddf33dd5d7463bc414b61af9d65e3ca0b1eecc93"
},
{
"type": "LOCALE",

View File

@ -1,5 +1,5 @@
{
"version": 8,
"version": 9,
"contents": [
{
"type": "COMPAT",
@ -14,8 +14,8 @@
"name": "SMAPI三星商店兼容包",
"assetPath": "compat/samsung_138/",
"description": "SMAPI三星商店兼容包 适用版本1.4.4.138至今, SMAPI 3.4.1.1",
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138_3.zip",
"hash": "d559c4b62fbf598eb8fff99ddcd152d7e34f6e30aa32a64d728ce1dd174b0dff"
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138_4.zip",
"hash": "e6d018c164d9f7d482f3d28dddf33dd5d7463bc414b61af9d65e3ca0b1eecc93"
},
{
"type": "LOCALE",

View File

@ -9,9 +9,9 @@ import android.os.Environment;
import android.view.Menu;
import android.view.MenuItem;
import com.afollestad.materialdialogs.MaterialDialog;
import com.google.android.material.navigation.NavigationView;
import com.hjq.language.LanguagesManager;
import com.lmntrx.android.library.livin.missme.ProgressDialog;
import com.microsoft.appcenter.AppCenter;
import com.microsoft.appcenter.analytics.Analytics;
import com.microsoft.appcenter.crashes.Crashes;
@ -25,6 +25,7 @@ import com.zane.smapiinstaller.logic.ConfigManager;
import com.zane.smapiinstaller.logic.GameLauncher;
import com.zane.smapiinstaller.logic.ModAssetsManager;
import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.JSONUtil;
import com.zane.smapiinstaller.utils.TranslateUtil;
import org.apache.commons.lang3.StringUtils;
@ -128,7 +129,7 @@ public class MainActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.isCheckable()) {
if (item.isCheckable()) {
if (item.isChecked()) {
item.setChecked(false);
} else {
@ -148,16 +149,15 @@ public class MainActivity extends AppCompatActivity {
config.setDeveloperMode(item.isChecked());
break;
case R.id.settings_set_mod_path:
DialogUtils.showInputDialog(this, R.string.input, R.string.input_mods_path, Constants.MOD_PATH, Constants.MOD_PATH, (dialog, input) -> {
if(StringUtils.isNoneBlank(input)) {
DialogUtils.showInputDialog(toolbar, R.string.input, R.string.input_mods_path, Constants.MOD_PATH, Constants.MOD_PATH, (dialog, input) -> {
if (StringUtils.isNoneBlank(input)) {
String pathString = input.toString();
File file = new File(Environment.getExternalStorageDirectory(), pathString);
if(file.exists() && file.isDirectory()) {
if (file.exists() && file.isDirectory()) {
Constants.MOD_PATH = pathString;
config.setModsPath(pathString);
manager.flushConfig();
}
else {
} else {
DialogUtils.showAlertDialog(drawer, R.string.error, R.string.error_illegal_path);
}
}
@ -180,10 +180,10 @@ public class MainActivity extends AppCompatActivity {
}
private int getTranslateServiceIndex(AppConfig selectedTranslator) {
if(selectedTranslator == null) {
if (selectedTranslator == null) {
return 0;
}
switch (selectedTranslator.getValue()){
switch (selectedTranslator.getValue()) {
case "OFF":
return 0;
case "Google":
@ -194,19 +194,19 @@ public class MainActivity extends AppCompatActivity {
}
private void selectTranslateServiceLogic() {
DaoSession daoSession = ((MainApplication)this.getApplication()).getDaoSession();
DaoSession daoSession = ((MainApplication) this.getApplication()).getDaoSession();
AppConfigDao appConfigDao = daoSession.getAppConfigDao();
int index = getTranslateServiceIndex(appConfigDao.queryBuilder().where(AppConfigDao.Properties.Name.eq(AppConfigKey.ACTIVE_TRANSLATOR)).build().unique());
DialogUtils.setCurrentDialog(new MaterialDialog.Builder(this).title(R.string.settings_translation_service).items(R.array.translators).itemsCallbackSingleChoice(index, (dialog, itemView, position, text) -> {
DialogUtils.showSingleChoiceDialog(toolbar, R.string.settings_translation_service, R.array.translators, index, (dialog, position) -> {
AppConfig activeTranslator = appConfigDao.queryBuilder().where(AppConfigDao.Properties.Name.eq(AppConfigKey.ACTIVE_TRANSLATOR)).build().unique();
switch (position) {
case 0:
if(activeTranslator != null) {
if (activeTranslator != null) {
appConfigDao.delete(activeTranslator);
}
break;
case 1:
if(activeTranslator == null) {
if (activeTranslator == null) {
activeTranslator = new AppConfig(null, AppConfigKey.ACTIVE_TRANSLATOR, TranslateUtil.GOOGLE);
} else {
activeTranslator.setValue(TranslateUtil.GOOGLE);
@ -214,7 +214,7 @@ public class MainActivity extends AppCompatActivity {
appConfigDao.insertOrReplace(activeTranslator);
break;
case 2:
if(activeTranslator == null) {
if (activeTranslator == null) {
activeTranslator = new AppConfig(null, AppConfigKey.ACTIVE_TRANSLATOR, TranslateUtil.YOU_DAO);
} else {
activeTranslator.setValue(TranslateUtil.YOU_DAO);
@ -222,14 +222,12 @@ public class MainActivity extends AppCompatActivity {
appConfigDao.insertOrReplace(activeTranslator);
break;
default:
return false;
}
return true;
}).show());
});
}
private void selectLanguageLogic() {
DialogUtils.setCurrentDialog(new MaterialDialog.Builder(this).title(R.string.settings_set_language).items(R.array.languages).itemsCallback((dialog, itemView, position, text) -> {
DialogUtils.showListItemsDialog(toolbar, R.string.settings_set_language, R.array.languages, (dialog, position) -> {
boolean restart;
switch (position) {
case 0:
@ -271,12 +269,19 @@ public class MainActivity extends AppCompatActivity {
overridePendingTransition(R.anim.fragment_fade_enter, R.anim.fragment_fade_exit);
finish();
}
}).show());
});
}
private void updateCheckLogic() {
ModAssetsManager modAssetsManager = new ModAssetsManager(toolbar);
modAssetsManager.checkModUpdate();
modAssetsManager.checkModUpdate((list) -> {
try {
NavController controller = Navigation.findNavController(toolbar);
controller.navigate(MobileNavigationDirections.actionNavAnyToModUpdateFragment(JSONUtil.toJson(list)));
} catch (Exception e) {
Crashes.trackError(e);
}
});
}
@Override
@ -286,6 +291,20 @@ public class MainActivity extends AppCompatActivity {
|| super.onSupportNavigateUp();
}
@Override
public void onBackPressed() {
Object dialog = DialogUtils.getCurrentDialog();
if (dialog instanceof ProgressDialog) {
ProgressDialog progressDialog = ((ProgressDialog) dialog);
progressDialog.onBackPressed(
() -> {
super.onBackPressed();
return null;
}
);
}
}
@Override
protected void attachBaseContext(Context newBase) {
// 国际化适配绑定语种

View File

@ -0,0 +1,16 @@
package com.zane.smapiinstaller.constant;
/**
* @author Zane
*/
public enum DialogAction {
/**
* 确认
*/
POSITIVE,
/**
* 取消
*/
NEGATIVE
}

View File

@ -4,6 +4,7 @@ import lombok.Data;
/**
* 帮助信息
* @author Zane
*/
@Data
public class HelpItem {

View File

@ -1,4 +1,4 @@
package com.zane.smapiinstaller.dto;
package com.zane.smapiinstaller.entity;
import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;

View File

@ -92,6 +92,17 @@ public class CommonLogic {
}
}
/**
* 在UI线程执行操作
* @param activity activity
* @param action 操作
*/
public static void runOnUiThread(Activity activity, Consumer<Activity> action) {
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> action.accept(activity));
}
}
/**
* 打开指定URL
*

View File

@ -6,7 +6,6 @@ import android.os.Environment;
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.collect.ImmutableList;
@ -19,6 +18,7 @@ import com.lzy.okgo.model.Response;
import com.microsoft.appcenter.crashes.Crashes;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.dto.ModUpdateCheckRequestDto;
import com.zane.smapiinstaller.dto.ModUpdateCheckResponseDto;
import com.zane.smapiinstaller.entity.ModManifestEntry;
@ -37,8 +37,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import androidx.core.util.Consumer;
import java9.util.Objects;
import java9.util.function.Consumer;
import java9.util.function.Predicate;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
@ -291,7 +291,7 @@ public class ModAssetsManager {
}
}
public void checkModUpdate() {
public void checkModUpdate(Consumer<List<ModUpdateCheckResponseDto>> callback) {
List<ModUpdateCheckRequestDto.ModInfo> list = StreamSupport.stream(findAllInstalledMods(false))
.filter(mod -> mod.getUpdateKeys() != null && !mod.getUpdateKeys().isEmpty())
.map(ModUpdateCheckRequestDto.ModInfo::fromModManifestEntry)
@ -313,7 +313,7 @@ public class ModAssetsManager {
List<ModUpdateCheckResponseDto> checkResponseDtos = response.body();
if (checkResponseDtos != null) {
List<ModUpdateCheckResponseDto> list = StreamSupport.stream(checkResponseDtos).filter(dto -> dto.getSuggestedUpdate() != null).collect(Collectors.toList());
callback.accept(list);
}
}
});

View File

@ -1,6 +1,5 @@
package com.zane.smapiinstaller.ui.about;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@ -14,7 +13,7 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.microsoft.appcenter.crashes.Crashes;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.logic.CommonLogic;
@ -50,7 +49,7 @@ public class AboutFragment extends Fragment {
void gplay() {
try {
CommonLogic.doOnNonNull(this.getActivity(), (activity) -> this.openPlayStore("market://details?id=" + activity.getPackageName()));
} catch (ActivityNotFoundException ex) {
} catch (Exception ex) {
CommonLogic.doOnNonNull(this.getActivity(), (activity) -> CommonLogic.openUrl(activity, "https://play.google.com/store/apps/details?id=" + activity.getPackageName()));
}
}
@ -75,11 +74,9 @@ public class AboutFragment extends Fragment {
@OnClick(R.id.button_donation)
void donation() {
CommonLogic.doOnNonNull(this.getContext(), (context) -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(context)
.title(R.string.button_donation_text)
.items(R.array.donation_methods)
.itemsCallback((dialog, itemView, position, text) ->
CommonLogic.showAnimation(imgHeart, R.anim.heart_beat, (animation) -> listSelectLogic(context, position))).show()));
DialogUtils.showListItemsDialog(imgHeart, R.string.button_donation_text, R.array.donation_methods, (dialog, position) ->
CommonLogic.showAnimation(imgHeart, R.anim.heart_beat, (animation) ->
CommonLogic.doOnNonNull(this.getActivity(), (activity) -> listSelectLogic(activity, position))));
}
private void listSelectLogic(Context context, int position) {
@ -87,7 +84,12 @@ public class AboutFragment extends Fragment {
case 0:
boolean hasInstalledAlipayClient = AlipayDonate.hasInstalledAlipayClient(context);
if (hasInstalledAlipayClient) {
AlipayDonate.startAlipayClient(this.getActivity(), "fkx13570v1pp2xenyrx4y3f");
try {
AlipayDonate.startAlipayClient(this.getActivity(), "fkx13570v1pp2xenyrx4y3f");
} catch (Exception e) {
Crashes.trackError(e);
CommonLogic.openUrl(context, "http://dl.zaneyork.cn/alipay.png");
}
} else {
CommonLogic.openUrl(context, "http://dl.zaneyork.cn/alipay.png");
}

View File

@ -10,10 +10,10 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import com.afollestad.materialdialogs.DialogAction;
import com.zane.smapiinstaller.BuildConfig;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils;

View File

@ -5,6 +5,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.utils.DialogUtils;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
@ -15,13 +20,6 @@ import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnTextChanged;
import com.afollestad.materialdialogs.MaterialDialog;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.utils.DialogUtils;
import java.util.ArrayList;
/**
* @author Zane
*/
@ -55,7 +53,7 @@ public class ConfigFragment extends Fragment {
@OnClick(R.id.button_sort_by)
void onSortByClick() {
int index;
int index = 0;
switch (configViewModel.getSortBy()) {
case "Name asc":
index = 0;
@ -70,29 +68,23 @@ public class ConfigFragment extends Fragment {
index = 3;
break;
default:
index = 0;
}
CommonLogic.doOnNonNull(this.getContext(), context -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(context)
.title(R.string.sort_by)
.items(R.array.mod_list_sort_by)
.itemsCallbackSingleChoice(index, (dialog, itemView, position, text) -> {
switch (position) {
case 0:
configViewModel.switchSortBy("Name asc");
break;
case 1:
configViewModel.switchSortBy("Name desc");
break;
case 2:
configViewModel.switchSortBy("Date asc");
break;
case 3:
configViewModel.switchSortBy("Date desc");
break;
default:
return false;
}
return true;
}).show()));
DialogUtils.showSingleChoiceDialog(recyclerView, R.string.sort_by, R.array.mod_list_sort_by, index, (dialog, position) -> {
switch (position) {
case 0:
configViewModel.switchSortBy("Name asc");
break;
case 1:
configViewModel.switchSortBy("Name desc");
break;
case 2:
configViewModel.switchSortBy("Date asc");
break;
case 3:
configViewModel.switchSortBy("Date desc");
break;
default:
}
});
}
}

View File

@ -11,7 +11,7 @@ import com.zane.smapiinstaller.entity.AppConfig;
import com.zane.smapiinstaller.entity.AppConfigDao;
import com.zane.smapiinstaller.entity.DaoSession;
import com.zane.smapiinstaller.entity.ModManifestEntry;
import com.zane.smapiinstaller.dto.TranslationResult;
import com.zane.smapiinstaller.entity.TranslationResult;
import com.zane.smapiinstaller.entity.TranslationResultDao;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ListenableObject;

View File

@ -7,9 +7,9 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.entity.ModManifestEntry;
import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils;

View File

@ -13,14 +13,14 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.lmntrx.android.library.livin.missme.ProgressDialog;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.callback.FileCallback;
import com.lzy.okgo.model.Progress;
import com.lzy.okgo.model.Response;
import com.microsoft.appcenter.crashes.Crashes;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.constant.DownloadableContentTypes;
import com.zane.smapiinstaller.entity.DownloadableContent;
import com.zane.smapiinstaller.entity.ModManifestEntry;
@ -157,13 +157,13 @@ public class DownloadableContentAdapter extends RecyclerView.Adapter<Downloadabl
}
downloading.set(true);
ModManifestEntry finalModManifestEntry = modManifestEntry;
AtomicReference<MaterialDialog> dialogRef = DialogUtils.showProgressDialog(itemView, R.string.progress, "");
AtomicReference<ProgressDialog> dialogRef = DialogUtils.showProgressDialog(itemView, R.string.progress, "");
OkGo.<File>get(downloadableContent.getUrl()).execute(new FileCallback(file.getParentFile().getAbsolutePath(), file.getName()) {
@Override
public void onError(Response<File> response) {
super.onError(response);
MaterialDialog dialog = dialogRef.get();
DialogUtils.dismissDialog(itemView, dialog);
ProgressDialog dialog = dialogRef.get();
dialog.dismiss();
downloading.set(false);
DialogUtils.showAlertDialog(itemView, R.string.error, R.string.error_failed_to_download);
}
@ -171,17 +171,17 @@ public class DownloadableContentAdapter extends RecyclerView.Adapter<Downloadabl
@Override
public void downloadProgress(Progress progress) {
super.downloadProgress(progress);
MaterialDialog dialog = dialogRef.get();
if (dialog != null && !dialog.isCancelled()) {
dialog.setContent(R.string.downloading, progress.currentSize / 1024, progress.totalSize / 1024);
ProgressDialog dialog = dialogRef.get();
if (dialog != null) {
dialog.setMessage(context.getString(R.string.downloading, progress.currentSize / 1024, progress.totalSize / 1024));
dialog.setProgress((int) (progress.currentSize * 100.0 / progress.totalSize));
}
}
@Override
public void onSuccess(Response<File> response) {
MaterialDialog dialog = dialogRef.get();
DialogUtils.dismissDialog(itemView, dialog);
ProgressDialog dialog = dialogRef.get();
dialog.dismiss();
downloading.set(false);
File downloadedFile = response.body();
String hash = com.zane.smapiinstaller.utils.FileUtils.getFileHash(downloadedFile);

View File

@ -7,11 +7,10 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.GravityEnum;
import com.afollestad.materialdialogs.MaterialDialog;
import com.lmntrx.android.library.livin.missme.ProgressDialog;
import com.microsoft.appcenter.crashes.Crashes;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.logic.ApkPatcher;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.logic.ModAssetsManager;
@ -19,6 +18,8 @@ import com.zane.smapiinstaller.utils.DialogUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
@ -63,70 +64,68 @@ public class InstallFragment extends Fragment {
* 安装逻辑
*/
private void installLogic() {
new MaterialDialog.Builder(context).title(R.string.install_progress_title).content(R.string.extracting_package).contentGravity(GravityEnum.CENTER)
.progress(false, 100, true).cancelable(false).cancelListener(dialog -> {
if (task != null) {
task.interrupt();
AtomicReference<ProgressDialog> dialogHolder = DialogUtils.showProgressDialog(root, R.string.install_progress_title, context.getString(R.string.extracting_package));
if (task != null) {
task.interrupt();
}
task = new Thread(() -> {
ProgressDialog dialog = null;
try {
do {
Thread.sleep(10);
dialog = dialogHolder.get();
} while (dialog == null);
ApkPatcher patcher = new ApkPatcher(context);
ProgressDialog finalDialog = dialog;
patcher.registerProgressListener((progress) -> DialogUtils.setProgressDialogState(root, finalDialog, null, progress));
DialogUtils.setProgressDialogState(root, dialog, R.string.extracting_package, null);
String path = patcher.extract();
if (path == null) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found)));
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, null);
if (!CommonLogic.unpackSmapiFiles(context, path, false)) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_unpack_smapi_files)));
return;
}
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 6);
modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(root, dialog, R.string.patching_package, 8);
if (!patcher.patch(path)) {
int target = patcher.getSwitchAction().getAndSet(0);
if (target == R.string.menu_download) {
DialogUtils.showConfirmDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)), R.string.menu_download, R.string.cancel, (d, which) -> {
if (which == DialogAction.POSITIVE) {
NavController controller = Navigation.findNavController(root);
controller.navigate(InstallFragmentDirections.actionNavInstallToNavDownload());
}
});
} else {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)));
}
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.signing_package, null);
String signPath = patcher.sign(path);
if (signPath == null) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.installing_package, null);
patcher.install(signPath);
} catch (InterruptedException ignored) {
} catch (Exception e) {
Crashes.trackError(e);
DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage());
} finally {
if(dialog != null) {
dialog.dismiss();
}
}
}).showListener(dialogInterface -> {
final MaterialDialog dialog = (MaterialDialog) dialogInterface;
DialogUtils.setCurrentDialog(dialog);
if (task != null) {
task.interrupt();
}
task = new Thread(() -> {
try {
ApkPatcher patcher = new ApkPatcher(context);
patcher.registerProgressListener((progress)-> DialogUtils.setProgressDialogState(root, dialog, null, progress));
DialogUtils.setProgressDialogState(root, dialog, R.string.extracting_package, null);
String path = patcher.extract();
if (path == null) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.error_game_not_found)));
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, null);
if (!CommonLogic.unpackSmapiFiles(context, path, false)) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_unpack_smapi_files)));
return;
}
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
DialogUtils.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 6);
modAssetsManager.installDefaultMods();
DialogUtils.setProgressDialogState(root, dialog, R.string.patching_package, 8);
if (!patcher.patch(path)) {
int target = patcher.getSwitchAction().getAndSet(0);
if(target == R.string.menu_download) {
DialogUtils.showConfirmDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)), R.string.menu_download, R.string.cancel, (d, which) -> {
if(which == DialogAction.POSITIVE) {
NavController controller = Navigation.findNavController(root);
controller.navigate(InstallFragmentDirections.actionNavInstallToNavDownload());
}
});
}
else {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_patch_game)));
}
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.signing_package, null);
String signPath = patcher.sign(path);
if (signPath == null) {
DialogUtils.showAlertDialog(root, R.string.error, StringUtils.firstNonBlank(patcher.getErrorMessage().get(), context.getString(R.string.failed_to_sign_game)));
return;
}
DialogUtils.setProgressDialogState(root, dialog, R.string.installing_package, null);
patcher.install(signPath);
}
catch (Exception e) {
Crashes.trackError(e);
DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage());
}
finally {
DialogUtils.dismissDialog(root, dialog);
}
});
task.start();
}).show();
});
task.start();
}
}

View File

@ -0,0 +1,63 @@
package com.zane.smapiinstaller.ui.update;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.dto.ModUpdateCheckResponseDto;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.ButterKnife;
/**
* {@link RecyclerView.Adapter} that can display a {@link ModUpdateCheckResponseDto.UpdateInfo}
* @author Zane
*/
public class ModUpdateAdapter extends RecyclerView.Adapter<ModUpdateAdapter.ViewHolder> {
private List<ModUpdateCheckResponseDto.UpdateInfo> updateInfoList;
public void setUpdateInfoList(List<ModUpdateCheckResponseDto.UpdateInfo> updateInfoList) {
this.updateInfoList = updateInfoList;
notifyDataSetChanged();
}
public ModUpdateAdapter(List<ModUpdateCheckResponseDto.UpdateInfo> items) {
updateInfoList = items;
}
@NonNull
@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.setUpdateInfo(updateInfoList.get(position));
}
@Override
public int getItemCount() {
return updateInfoList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
public ModUpdateCheckResponseDto.UpdateInfo updateInfo;
public void setUpdateInfo(ModUpdateCheckResponseDto.UpdateInfo updateInfo) {
this.updateInfo = updateInfo;
}
public ViewHolder(View view) {
super(view);
ButterKnife.bind(this, itemView);
}
}
}

View File

@ -0,0 +1,56 @@
package com.zane.smapiinstaller.ui.update;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.fasterxml.jackson.core.type.TypeReference;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.dto.ModUpdateCheckResponseDto;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.utils.JSONUtil;
import java.util.List;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* A fragment representing a list of Items.
* @author Zane
*/
public class ModUpdateFragment extends Fragment {
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public ModUpdateFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_mod_update_list, container, false);
// Set the adapter
if (view instanceof RecyclerView) {
Context context = view.getContext();
RecyclerView recyclerView = (RecyclerView) view;
recyclerView.setLayoutManager(new LinearLayoutManager(context));
CommonLogic.doOnNonNull(this.getArguments(), arguments -> {
String updateInfoListJson = arguments.getString("updateInfoListJson", "[]");
List<ModUpdateCheckResponseDto.UpdateInfo> updateInfos = JSONUtil.fromJson(updateInfoListJson, new TypeReference<List<ModUpdateCheckResponseDto.UpdateInfo>>() {
});
ModUpdateAdapter adapter = new ModUpdateAdapter(updateInfos);
recyclerView.setAdapter(adapter);
});
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
}
return view;
}
}

View File

@ -1,26 +1,40 @@
package com.zane.smapiinstaller.utils;
import android.app.Activity;
import android.app.Dialog;
import android.text.InputType;
import android.view.View;
import android.widget.ProgressBar;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.input.DialogInputExtKt;
import com.afollestad.materialdialogs.list.DialogListExtKt;
import com.afollestad.materialdialogs.list.DialogSingleChoiceExtKt;
import com.lmntrx.android.library.livin.missme.ProgressDialog;
import com.microsoft.appcenter.crashes.Crashes;
import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.constant.DialogAction;
import com.zane.smapiinstaller.logic.CommonLogic;
import java.util.concurrent.atomic.AtomicReference;
import java9.util.function.BiConsumer;
/**
* 对话框相关工具类
*
* @author Zane
*/
public class DialogUtils {
private static Dialog currentDialog = null;
public static void setCurrentDialog(Dialog currentDialog) {
private static Object currentDialog = null;
public static Object getCurrentDialog() {
return currentDialog;
}
public static void setCurrentDialog(Object currentDialog) {
DialogUtils.currentDialog = currentDialog;
}
/**
* 设置进度条状态
*
@ -29,18 +43,15 @@ public class DialogUtils {
* @param message 消息
* @param progress 进度
*/
public static void setProgressDialogState(View view, MaterialDialog dialog, Integer message, Integer progress) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing() && !dialog.isCancelled()) {
activity.runOnUiThread(() -> {
if(progress != null) {
dialog.setProgress(progress);
}
if(message != null) {
dialog.setContent(message);
}
});
}
public static void setProgressDialogState(View view, ProgressDialog dialog, Integer message, Integer progress) {
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
if (progress != null) {
dialog.setProgress(progress);
}
if (message != null) {
dialog.setMessage(activity.getString(message));
}
});
}
/**
@ -51,10 +62,11 @@ public class DialogUtils {
* @param message 消息
*/
public static void showAlertDialog(View view, int title, String message) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(activity).title(title).content(message).positiveText(R.string.ok).show()));
}
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog materialDialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null).message(null, message, null).positiveButton(R.string.ok, null, null);
DialogUtils.setCurrentDialog(materialDialog);
materialDialog.show();
});
}
/**
@ -65,10 +77,11 @@ public class DialogUtils {
* @param message 消息
*/
public static void showAlertDialog(View view, int title, int message) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(activity).title(title).content(message).positiveText(R.string.ok).show()));
}
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog materialDialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null).message(message, null, null).positiveButton(R.string.ok, null, null);
DialogUtils.setCurrentDialog(materialDialog);
materialDialog.show();
});
}
/**
@ -79,11 +92,18 @@ public class DialogUtils {
* @param message 消息
* @param callback 回调
*/
public static void showConfirmDialog(View view, int title, int message, MaterialDialog.SingleButtonCallback callback) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(activity).title(title).content(message).positiveText(R.string.confirm).negativeText(R.string.cancel).onAny(callback).show()));
}
public static void showConfirmDialog(View view, int title, int message, BiConsumer<MaterialDialog, DialogAction> callback) {
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog materialDialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null).message(message, null, null).positiveButton(R.string.confirm, null, dialog -> {
callback.accept(dialog, DialogAction.POSITIVE);
return null;
}).negativeButton(R.string.cancel, null, dialog -> {
callback.accept(dialog, DialogAction.NEGATIVE);
return null;
});
DialogUtils.setCurrentDialog(materialDialog);
materialDialog.show();
});
}
/**
@ -94,7 +114,7 @@ public class DialogUtils {
* @param message 消息
* @param callback 回调
*/
public static void showConfirmDialog(View view, int title, String message, MaterialDialog.SingleButtonCallback callback) {
public static void showConfirmDialog(View view, int title, String message, BiConsumer<MaterialDialog, DialogAction> callback) {
showConfirmDialog(view, title, message, R.string.confirm, R.string.cancel, callback);
}
@ -108,11 +128,18 @@ public class DialogUtils {
* @param negativeText 取消文本
* @param callback 回调
*/
public static void showConfirmDialog(View view, int title, String message, int positiveText, int negativeText, MaterialDialog.SingleButtonCallback callback) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(activity).title(title).content(message).positiveText(positiveText).negativeText(negativeText).onAny(callback).show()));
}
public static void showConfirmDialog(View view, int title, String message, int positiveText, int negativeText, BiConsumer<MaterialDialog, DialogAction> callback) {
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog materialDialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null).message(null, message, null).positiveButton(positiveText, null, dialog -> {
callback.accept(dialog, DialogAction.POSITIVE);
return null;
}).negativeButton(negativeText, null, dialog -> {
callback.accept(dialog, DialogAction.NEGATIVE);
return null;
});
DialogUtils.setCurrentDialog(materialDialog);
materialDialog.show();
});
}
/**
@ -123,58 +150,123 @@ public class DialogUtils {
* @param message 消息
* @return 对话框引用
*/
public static AtomicReference<MaterialDialog> showProgressDialog(View view, int title, String message) {
Activity activity = CommonLogic.getActivityFromView(view);
AtomicReference<MaterialDialog> reference = new AtomicReference<>();
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> {
MaterialDialog dialog = new MaterialDialog.Builder(activity)
.title(title)
.content(message)
.progress(false, 100, true)
.cancelable(false)
.show();
currentDialog = dialog;
reference.set(dialog);
});
}
public static AtomicReference<ProgressDialog> showProgressDialog(View view, int title, String message) {
AtomicReference<ProgressDialog> reference = new AtomicReference<>();
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
ProgressDialog dialog = new ProgressDialog(activity);
DialogUtils.setCurrentDialog(dialog);
dialog.setMessage(message);
dialog.setCancelable(false);
dialog.setMax(100);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.show();
reference.set(dialog);
});
return reference;
}
/**
* 解散指定对话框
*
* @param view view
* @param dialog 对话框
*/
public static void dismissDialog(View view, MaterialDialog dialog) {
Activity activity = CommonLogic.getActivityFromView(view);
if (activity != null && !activity.isFinishing()) {
if (dialog != null && !dialog.isCancelled()) {
if (dialog != null && dialog.isShowing()) {
try {
dialog.dismiss();
}
catch (Exception e) {
} catch (Exception e) {
Crashes.trackError(e);
}
}
}
}
/**
* 解散当前对话框
*/
public static void dismissDialog() {
if (currentDialog != null && currentDialog.isShowing()) {
try {
currentDialog.dismiss();
}
catch (Exception e) {
Crashes.trackError(e);
if (currentDialog != null) {
if (currentDialog instanceof MaterialDialog) {
MaterialDialog dialog = (MaterialDialog) currentDialog;
if (dialog.isShowing()) {
try {
dialog.dismiss();
} catch (Exception e) {
Crashes.trackError(e);
}
}
} else if (currentDialog instanceof ProgressDialog) {
ProgressDialog dialog = (ProgressDialog) currentDialog;
dialog.dismiss();
}
}
}
public static void showInputDialog(Activity activity, int title, int content, String hint, String prefill, MaterialDialog.InputCallback callback) {
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(() -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(activity)
.title(title)
.content(content)
.inputType(InputType.TYPE_CLASS_TEXT)
.input(hint, prefill, callback)
.show())
);
}
/**
* 显示输入框
*
* @param view context容器
* @param title 标题
* @param content 内容
* @param hint 提示
* @param prefill 预输入
* @param callback 回调
*/
public static void showInputDialog(View view, int title, int content, String hint, String prefill, BiConsumer<MaterialDialog, CharSequence> callback) {
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog dialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null).message(content, null, null);
dialog = DialogInputExtKt.input(dialog, hint, null, prefill, null,
InputType.TYPE_CLASS_TEXT,
null, true, false, (materialDialog, text) -> {
callback.accept(materialDialog, text);
return null;
});
DialogUtils.setCurrentDialog(dialog);
dialog.show();
});
}
/**
* 显示列表单选框
*
* @param view context容器
* @param title 标题
* @param items 列表
* @param index 默认选择
* @param callback 回调
*/
public static void showSingleChoiceDialog(View view, int title, int items, int index, BiConsumer<MaterialDialog, Integer> callback) {
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog materialDialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null);
materialDialog = DialogSingleChoiceExtKt.listItemsSingleChoice(materialDialog, items, null, null, index, false, (dialog, position, text) -> {
callback.accept(dialog, position);
return null;
});
DialogUtils.setCurrentDialog(materialDialog);
materialDialog.show();
});
}
/**
* 显示列表选择框
*
* @param view context容器
* @param title 标题
* @param items 列表
* @param callback 回调
*/
public static void showListItemsDialog(View view, int title, int items, BiConsumer<MaterialDialog, Integer> callback) {
CommonLogic.runOnUiThread(CommonLogic.getActivityFromView(view), (activity) -> {
MaterialDialog materialDialog = new MaterialDialog(activity, MaterialDialog.getDEFAULT_BEHAVIOR()).title(title, null);
materialDialog = DialogListExtKt.listItems(materialDialog, items, null, null, false, (dialog, position, text) -> {
callback.accept(dialog, position);
return null;
});
DialogUtils.setCurrentDialog(materialDialog);
materialDialog.show();
});
}
}

View File

@ -7,6 +7,9 @@ import com.lzy.okgo.request.base.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* @author Zane
*/
public abstract class JsonCallback<T> extends AbsCallback<T> {
private TypeReference<T> type;

View File

@ -9,7 +9,7 @@ import com.lzy.okgo.callback.StringCallback;
import com.lzy.okgo.model.Response;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.dto.GoogleTranslationDto;
import com.zane.smapiinstaller.dto.TranslationResult;
import com.zane.smapiinstaller.entity.TranslationResult;
import com.zane.smapiinstaller.dto.YouDaoTranslationDto;
import org.apache.commons.lang3.StringUtils;

View File

@ -0,0 +1,12 @@
<?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: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.update.ModUpdateFragment"
tools:listitem="@layout/updatable_mod_list_item" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -52,4 +52,16 @@
android:name="com.zane.smapiinstaller.ui.about.AboutFragment"
android:label="@string/menu_about"
tools:layout="@layout/fragment_about" />
<fragment
android:id="@+id/nav_mod_update"
android:name="com.zane.smapiinstaller.ui.update.ModUpdateFragment"
android:label="@string/settings_check_for_updates"
tools:layout="@layout/fragment_mod_update_list">
</fragment>
<action
android:id="@+id/action_nav_any_to_mod_update_fragment"
app:destination="@id/nav_mod_update" >
<argument android:name="updateInfoListJson" app:argType="string"/>
</action>
</navigation>

View File

@ -9,7 +9,8 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0-alpha03"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0-alpha04"
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
// NOTE: Do not place your application dependencies here; they belong