1.Mod update check(in progress)

2.code clean up
This commit is contained in:
ZaneYork 2020-03-30 15:08:54 +08:00
parent 5743e50e04
commit e69565d5e3
19 changed files with 430 additions and 114 deletions

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectPlainTextFileTypeManager">
<file url="file://$PROJECT_DIR$/app/src/main/assets/AndroidManifest.xml" />
<file url="file://$PROJECT_DIR$/app/src/main/assets/apk/AndroidManifest.xml" />
<file url="file://$PROJECT_DIR$/app/src/main/assets/apk/AndroidManifest.xml" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>

View File

@ -23,6 +23,7 @@ import com.zane.smapiinstaller.entity.DaoSession;
import com.zane.smapiinstaller.entity.FrameworkConfig; import com.zane.smapiinstaller.entity.FrameworkConfig;
import com.zane.smapiinstaller.logic.ConfigManager; import com.zane.smapiinstaller.logic.ConfigManager;
import com.zane.smapiinstaller.logic.GameLauncher; import com.zane.smapiinstaller.logic.GameLauncher;
import com.zane.smapiinstaller.logic.ModAssetsManager;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.TranslateUtil; import com.zane.smapiinstaller.utils.TranslateUtil;
@ -168,6 +169,9 @@ public class MainActivity extends AppCompatActivity {
case R.id.settings_translation_service: case R.id.settings_translation_service:
selectTranslateServiceLogic(); selectTranslateServiceLogic();
return true; return true;
case R.id.toolbar_update_check:
updateCheckLogic();
return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -270,6 +274,11 @@ public class MainActivity extends AppCompatActivity {
}).show()); }).show());
} }
private void updateCheckLogic() {
ModAssetsManager modAssetsManager = new ModAssetsManager(toolbar);
modAssetsManager.checkModUpdate();
}
@Override @Override
public boolean onSupportNavigateUp() { public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);

View File

@ -8,7 +8,6 @@ import com.lzy.okgo.OkGo;
import com.zane.smapiinstaller.entity.DaoMaster; import com.zane.smapiinstaller.entity.DaoMaster;
import com.zane.smapiinstaller.entity.DaoSession; import com.zane.smapiinstaller.entity.DaoSession;
import com.zane.smapiinstaller.utils.DbOpenHelper; import com.zane.smapiinstaller.utils.DbOpenHelper;
import com.zane.smapiinstaller.utils.GzipRequestInterceptor;
import org.greenrobot.greendao.database.Database; import org.greenrobot.greendao.database.Database;
@ -27,7 +26,7 @@ public class MainApplication extends Application {
super.onCreate(); super.onCreate();
OkHttpClient okHttpClient = new OkHttpClient.Builder() OkHttpClient okHttpClient = new OkHttpClient.Builder()
//开启Gzip压缩 //开启Gzip压缩
.addInterceptor(new GzipRequestInterceptor()) // .addInterceptor(new GzipRequestInterceptor())
.build(); .build();
OkGo.getInstance().setOkHttpClient(okHttpClient).init(this); OkGo.getInstance().setOkHttpClient(okHttpClient).init(this);
LanguagesManager.init(this); LanguagesManager.init(this);

View File

@ -69,4 +69,9 @@ public class Constants {
* 平台 * 平台
*/ */
public static final String PLATFORM = "Android"; public static final String PLATFORM = "Android";
/**
* SMAPI更新服务
*/
public static final String UPDATE_CHECK_SERVICE_URL = "https://smapi.io/api/v" + SMAPI_VERSION + "/mods";
} }

View File

@ -0,0 +1,142 @@
package com.zane.smapiinstaller.dto;
import android.util.Log;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.zane.smapiinstaller.constant.Constants;
import com.zane.smapiinstaller.entity.ModManifestEntry;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
/**
* @author Zane
*/
@Data
@RequiredArgsConstructor
public class ModUpdateCheckRequestDto {
/**
* 待检查MOD列表
*/
@NonNull
private List<ModInfo> mods;
/**
* SMAPI版本
*/
private SemanticVersion apiVersion = new SemanticVersion(Constants.SMAPI_VERSION);
/**
* 游戏版本
*/
@NonNull
private SemanticVersion gameVersion;
/**
* 平台版本
*/
private String platform = Constants.PLATFORM;
/**
* 是否拉取MOD详情
*/
private boolean includeExtendedMetadata = false;
@Data
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
public static class SemanticVersion {
private int MajorVersion;
private int MinorVersion;
private int PatchVersion;
private int PlatformRelease;
private String PrereleaseTag;
private String BuildMetadata;
public SemanticVersion(String versionStr) {
// init
MajorVersion = 0;
MinorVersion = 0;
PatchVersion = 0;
PlatformRelease = 0;
PrereleaseTag = null;
BuildMetadata = null;
// normalize
versionStr = StringUtils.trim(versionStr);
if (StringUtils.isBlank(versionStr)) {
return;
}
List<String> versionSections = Splitter.on(CharMatcher.anyOf(".-+")).splitToList(versionStr);
// read major/minor version
int i = 0;
if (versionSections.size() > i) {
MajorVersion = Integer.parseInt(versionSections.get(i));
}
else {
return;
}
i++;
if (versionSections.size() > i) {
MinorVersion = Integer.parseInt(versionSections.get(i));
}
else {
return;
}
i++;
// read optional patch version
if (versionSections.size() > i) {
PatchVersion = Integer.parseInt(versionSections.get(i));
}
else {
return;
}
i++;
// read optional non-standard platform release version
try {
if (versionSections.size() > i) {
PlatformRelease = Integer.parseInt(versionSections.get(i));
}
else {
return;
}
} catch (NumberFormatException ignored) {
}
// read optional prerelease tag
versionSections = Splitter.on("-").limit(2).splitToList(versionStr);
if (versionSections.size() > 1) {
PrereleaseTag = RegExUtils.removeFirst(versionSections.get(1), "\\+.*");
}
else {
return;
}
// read optional build tag
versionSections = Splitter.on("+").limit(2).splitToList(versionStr);
if (versionSections.size() > 1) {
BuildMetadata = versionSections.get(1);
}
}
}
@Data
public static class ModInfo {
private String id;
private List<String> updateKeys;
private SemanticVersion installedVersion;
public static ModInfo fromModManifestEntry(ModManifestEntry mod) {
ModInfo modInfo = new ModInfo();
modInfo.setId(mod.getUniqueID());
try {
modInfo.setInstalledVersion(new SemanticVersion(mod.getVersion()));
} catch (Exception e) {
Log.d("", "", e);
}
modInfo.setUpdateKeys(mod.getUpdateKeys());
return modInfo;
}
}
}

View File

@ -0,0 +1,20 @@
package com.zane.smapiinstaller.dto;
import java.util.List;
import lombok.Data;
/**
* @author Zane
*/
@Data
public class ModUpdateCheckResponseDto {
private String id;
private UpdateInfo suggestedUpdate;
private List<String> errors;
@Data
public static class UpdateInfo {
private String version;
private String url;
}
}

View File

@ -189,6 +189,9 @@ public class ApkPatcher {
AtomicReference<String> packageName = new AtomicReference<>(); AtomicReference<String> packageName = new AtomicReference<>();
AtomicLong versionCode = new AtomicLong(); AtomicLong versionCode = new AtomicLong();
Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> { Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> {
if(attr == null) {
return true;
}
if (attr.type == NodeVisitor.TYPE_STRING) { if (attr.type == NodeVisitor.TYPE_STRING) {
String strObj = (String) attr.obj; String strObj = (String) attr.obj;
switch (attr.name) { switch (attr.name) {
@ -227,6 +230,9 @@ public class ApkPatcher {
try { try {
byte[] modifyManifest = CommonLogic.modifyManifest(bytes, processLogic); byte[] modifyManifest = CommonLogic.modifyManifest(bytes, processLogic);
Iterables.removeIf(manifests, manifest -> { Iterables.removeIf(manifests, manifest -> {
if(manifest == null) {
return true;
}
if (versionCode.get() < manifest.getMinBuildCode()) { if (versionCode.get() < manifest.getMinBuildCode()) {
return true; return true;
} }

View File

@ -42,11 +42,13 @@ import pxb.android.axml.NodeVisitor;
/** /**
* 通用逻辑 * 通用逻辑
*
* @author Zane * @author Zane
*/ */
public class CommonLogic { public class CommonLogic {
/** /**
* 从View获取所属Activity * 从View获取所属Activity
*
* @param view context容器 * @param view context容器
* @return Activity * @return Activity
*/ */
@ -65,19 +67,34 @@ public class CommonLogic {
/** /**
* 从一个View获取Application * 从一个View获取Application
*
* @param view 控件 * @param view 控件
* @return Application * @return Application
*/ */
public static MainApplication getApplicationFromView(View view) { public static MainApplication getApplicationFromView(View view) {
Activity activity = getActivityFromView(view); Activity activity = getActivityFromView(view);
if(null != activity) { if (null != activity) {
return (MainApplication) activity.getApplication(); return (MainApplication) activity.getApplication();
} }
return null; return null;
} }
/**
* 当data非null时执行操作
*
* @param data 数据
* @param action 操作
* @param <T> 泛型
*/
public static <T> void doOnNonNull(T data, Consumer<T> action) {
if (data != null) {
action.accept(data);
}
}
/** /**
* 打开指定URL * 打开指定URL
*
* @param context context * @param context context
* @param url 目标URL * @param url 目标URL
*/ */
@ -87,22 +104,23 @@ public class CommonLogic {
intent.setData(Uri.parse(url)); intent.setData(Uri.parse(url));
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
context.startActivity(intent); context.startActivity(intent);
} } catch (ActivityNotFoundException ignored) {
catch (ActivityNotFoundException ignored){
} }
} }
/** /**
* 复制文本到剪贴板 * 复制文本到剪贴板
*
* @param context 上下文 * @param context 上下文
* @param copyStr 文本 * @param copyStr 文本
* @return 是否复制成功 * @return 是否复制成功
*/ */
public static boolean copyToClipboard(Context context, String copyStr) { public static boolean copyToClipboard(Context context, String copyStr) {
try { try {
ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); CommonLogic.doOnNonNull(context.getSystemService(Context.CLIPBOARD_SERVICE), cm -> {
ClipData mClipData = ClipData.newPlainText("Label", copyStr); ClipData mClipData = ClipData.newPlainText("Label", copyStr);
cm.setPrimaryClip(mClipData); ((ClipboardManager) cm).setPrimaryClip(mClipData);
});
return true; return true;
} catch (Exception e) { } catch (Exception e) {
return false; return false;
@ -111,6 +129,7 @@ public class CommonLogic {
/** /**
* 扫描全部兼容包 * 扫描全部兼容包
*
* @param context context * @param context context
* @return 兼容包列表 * @return 兼容包列表
*/ */
@ -130,10 +149,9 @@ public class CommonLogic {
} }
} }
Collections.sort(apkFilesManifests, (a, b) -> { Collections.sort(apkFilesManifests, (a, b) -> {
if(a.getTargetPackageName() != null && b.getTargetPackageName() == null) { if (a.getTargetPackageName() != null && b.getTargetPackageName() == null) {
return -1; return -1;
} } else if (b.getTargetPackageName() != null) {
else if(b.getTargetPackageName() != null){
return Long.compare(b.getMinBuildCode(), a.getMinBuildCode()); return Long.compare(b.getMinBuildCode(), a.getMinBuildCode());
} }
return 1; return 1;
@ -143,13 +161,15 @@ public class CommonLogic {
/** /**
* 提取SMAPI环境文件到内部存储对应位置 * 提取SMAPI环境文件到内部存储对应位置
* @param context context *
* @param apkPath 安装包路径 * @param context context
* @param apkPath 安装包路径
* @param checkMode 是否为校验模式 * @param checkMode 是否为校验模式
* @return 操作是否成功 * @return 操作是否成功
*/ */
public static boolean unpackSmapiFiles(Context context, String apkPath, boolean checkMode) { public static boolean unpackSmapiFiles(Context context, String apkPath, boolean checkMode) {
List<ManifestEntry> manifestEntries = FileUtils.getAssetJson(context, "smapi_files_manifest.json", new TypeReference<List<ManifestEntry>>() { }); List<ManifestEntry> manifestEntries = FileUtils.getAssetJson(context, "smapi_files_manifest.json", new TypeReference<List<ManifestEntry>>() {
});
if (manifestEntries == null) { if (manifestEntries == null) {
return false; return false;
} }
@ -199,6 +219,7 @@ public class CommonLogic {
/** /**
* 修改AndroidManifest.xml文件 * 修改AndroidManifest.xml文件
*
* @param bytes AndroidManifest.xml文件字符数组 * @param bytes AndroidManifest.xml文件字符数组
* @param processLogic 处理逻辑 * @param processLogic 处理逻辑
* @return 修改后的AndroidManifest.xml文件字符数组 * @return 修改后的AndroidManifest.xml文件字符数组

View File

@ -24,10 +24,11 @@ public class GameLauncher {
} }
/** /**
* 启动逻辑 * 检查已安装MOD版本游戏
* @param context 上下文
* @return 软件包信息
*/ */
public void launch() { public static PackageInfo getGamePackageInfo(Activity context) {
Activity context = CommonLogic.getActivityFromView(root);
PackageManager packageManager = context.getPackageManager(); PackageManager packageManager = context.getPackageManager();
try { try {
PackageInfo packageInfo; PackageInfo packageInfo;
@ -36,20 +37,35 @@ public class GameLauncher {
} catch (PackageManager.NameNotFoundException ignored) { } catch (PackageManager.NameNotFoundException ignored) {
packageInfo = packageManager.getPackageInfo(Constants.TARGET_PACKAGE_NAME_SAMSUNG, 0); packageInfo = packageManager.getPackageInfo(Constants.TARGET_PACKAGE_NAME_SAMSUNG, 0);
} }
return packageInfo;
} catch (PackageManager.NameNotFoundException ignored) {
return null;
}
}
/**
* 启动逻辑
*/
public void launch() {
Activity context = CommonLogic.getActivityFromView(root);
PackageManager packageManager = context.getPackageManager();
try {
PackageInfo packageInfo = getGamePackageInfo(context);
if(packageInfo == null) {
DialogUtils.showAlertDialog(root, R.string.error, R.string.error_smapi_not_installed);
return;
}
if(!CommonLogic.unpackSmapiFiles(context, packageInfo.applicationInfo.publicSourceDir, true)) { if(!CommonLogic.unpackSmapiFiles(context, packageInfo.applicationInfo.publicSourceDir, true)) {
DialogUtils.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair); DialogUtils.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair);
return; return;
} }
ModAssetsManager modAssetsManager = new ModAssetsManager(root); ModAssetsManager modAssetsManager = new ModAssetsManager(root);
PackageInfo finalPackageInfo = packageInfo;
modAssetsManager.checkModEnvironment((isConfirm) -> { modAssetsManager.checkModEnvironment((isConfirm) -> {
if(isConfirm) { if(isConfirm) {
Intent intent = packageManager.getLaunchIntentForPackage(finalPackageInfo.packageName); Intent intent = packageManager.getLaunchIntentForPackage(packageInfo.packageName);
context.startActivity(intent); context.startActivity(intent);
} }
}); });
} catch (PackageManager.NameNotFoundException ignored) {
DialogUtils.showAlertDialog(root, R.string.error, R.string.error_smapi_not_installed);
} catch (Exception e) { } catch (Exception e) {
Crashes.trackError(e); Crashes.trackError(e);
DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage()); DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage());

View File

@ -1,6 +1,7 @@
package com.zane.smapiinstaller.logic; package com.zane.smapiinstaller.logic;
import android.app.Activity; import android.app.Activity;
import android.content.pm.PackageInfo;
import android.os.Environment; import android.os.Environment;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@ -8,17 +9,23 @@ import android.view.View;
import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.DialogAction;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import com.google.common.collect.Queues; import com.google.common.collect.Queues;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.model.Response;
import com.microsoft.appcenter.crashes.Crashes;
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.dto.ModUpdateCheckRequestDto;
import com.zane.smapiinstaller.dto.ModUpdateCheckResponseDto;
import com.zane.smapiinstaller.entity.ModManifestEntry; import com.zane.smapiinstaller.entity.ModManifestEntry;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils; import com.zane.smapiinstaller.utils.FileUtils;
import com.zane.smapiinstaller.utils.JSONUtil;
import com.zane.smapiinstaller.utils.JsonCallback;
import com.zane.smapiinstaller.utils.VersionUtil; import com.zane.smapiinstaller.utils.VersionUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -30,12 +37,12 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import androidx.core.util.Consumer;
import java9.util.Objects; import java9.util.Objects;
import java9.util.function.Predicate;
import java9.util.stream.Collectors; import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport; import java9.util.stream.StreamSupport;
import androidx.core.util.Consumer;
/** /**
* Mod资源管理器 * Mod资源管理器
*/ */
@ -68,7 +75,7 @@ public class ModAssetsManager {
foundManifest = true; foundManifest = true;
if (manifest != null) { if (manifest != null) {
manifest.setAssetPath(file.getParentFile().getAbsolutePath()); manifest.setAssetPath(file.getParentFile().getAbsolutePath());
if (filter.apply(manifest)) { if (filter.test(manifest)) {
return manifest; return manifest;
} }
} }
@ -155,7 +162,7 @@ public class ModAssetsManager {
if (installedMods.size() > 1) { if (installedMods.size() > 1) {
DialogUtils.showAlertDialog(root, R.string.error, DialogUtils.showAlertDialog(root, R.string.error,
String.format(context.getString(R.string.duplicate_mod_found), String.format(context.getString(R.string.duplicate_mod_found),
Joiner.on(",").join(Lists.transform(installedMods, item -> FileUtils.toPrettyPath(item.getAssetPath()))))); StreamSupport.stream(installedMods).map(item -> FileUtils.toPrettyPath(item.getAssetPath())).collect(Collectors.joining(","))));
return false; return false;
} else if (installedMods.size() == 0) { } else if (installedMods.size() == 0) {
installedMods = installedModMap.get(mod.getUniqueID().replace("ZaneYork.CustomLocalization", "SMAPI.CustomLocalization")); installedMods = installedModMap.get(mod.getUniqueID().replace("ZaneYork.CustomLocalization", "SMAPI.CustomLocalization"));
@ -212,7 +219,7 @@ public class ModAssetsManager {
for (String key : installedModMap.keySet()) { for (String key : installedModMap.keySet()) {
ImmutableList<ModManifestEntry> installedMods = installedModMap.get(key); ImmutableList<ModManifestEntry> installedMods = installedModMap.get(key);
if (installedMods.size() > 1) { if (installedMods.size() > 1) {
list.add(Joiner.on(",").join(Lists.transform(installedMods, item -> FileUtils.toPrettyPath(item.getAssetPath())))); list.add(StreamSupport.stream(installedMods).map(item -> FileUtils.toPrettyPath(item.getAssetPath())).collect(Collectors.joining(",")));
} }
} }
if (!list.isEmpty()) { if (!list.isEmpty()) {
@ -284,6 +291,37 @@ public class ModAssetsManager {
} }
} }
public void checkModUpdate() {
List<ModUpdateCheckRequestDto.ModInfo> list = StreamSupport.stream(findAllInstalledMods(false))
.filter(mod -> mod.getUpdateKeys() != null && !mod.getUpdateKeys().isEmpty())
.map(ModUpdateCheckRequestDto.ModInfo::fromModManifestEntry)
.filter(modInfo -> modInfo.getInstalledVersion() != null)
.collect(Collectors.toList());
Activity context = CommonLogic.getActivityFromView(root);
PackageInfo gamePackageInfo = GameLauncher.getGamePackageInfo(context);
if (gamePackageInfo == null) {
return;
}
try {
ModUpdateCheckRequestDto requestDto = new ModUpdateCheckRequestDto(list, new ModUpdateCheckRequestDto.SemanticVersion(gamePackageInfo.versionName));
OkGo.<List<ModUpdateCheckResponseDto>>post(Constants.UPDATE_CHECK_SERVICE_URL)
.upJson(JSONUtil.toJson(requestDto))
.execute(new JsonCallback<List<ModUpdateCheckResponseDto>>(new TypeReference<List<ModUpdateCheckResponseDto>>() {
}) {
@Override
public void onSuccess(Response<List<ModUpdateCheckResponseDto>> response) {
List<ModUpdateCheckResponseDto> checkResponseDtos = response.body();
if (checkResponseDtos != null) {
List<ModUpdateCheckResponseDto> list = StreamSupport.stream(checkResponseDtos).filter(dto -> dto.getSuggestedUpdate() != null).collect(Collectors.toList());
}
}
});
} catch (Exception e) {
Crashes.trackError(e);
}
}
private String checkModDependencyError(ModManifestEntry mod, ImmutableListMultimap<String, ModManifestEntry> installedModMap) { private String checkModDependencyError(ModManifestEntry mod, ImmutableListMultimap<String, ModManifestEntry> installedModMap) {
if (mod.getDependencies() != null) { if (mod.getDependencies() != null) {
List<ModManifestEntry> unsatisfiedDependencies = StreamSupport.stream(mod.getDependencies()) List<ModManifestEntry> unsatisfiedDependencies = StreamSupport.stream(mod.getDependencies())

View File

@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import java9.util.Objects;
/** /**
* @author Zane * @author Zane
@ -44,17 +43,16 @@ public class AboutFragment extends Fragment {
@OnClick(R.id.button_release) @OnClick(R.id.button_release)
void release() { void release() {
CommonLogic.openUrl(this.getContext(), "https://github.com/ZaneYork/SMAPI-Android-Installer/releases"); CommonLogic.doOnNonNull(this.getContext(), (context) -> CommonLogic.openUrl(context, "https://github.com/ZaneYork/SMAPI-Android-Installer/releases"));
} }
@OnClick(R.id.button_gplay) @OnClick(R.id.button_gplay)
void gplay() { void gplay() {
try { try {
this.openPlayStore("market://details?id=" + this.getActivity().getPackageName()); CommonLogic.doOnNonNull(this.getActivity(), (activity) -> this.openPlayStore("market://details?id=" + activity.getPackageName()));
} catch (ActivityNotFoundException ex) { } catch (ActivityNotFoundException ex) {
CommonLogic.openUrl(this.getContext(), "https://play.google.com/store/apps/details?id=" + this.getActivity().getPackageName()); CommonLogic.doOnNonNull(this.getActivity(), (activity) -> CommonLogic.openUrl(activity, "https://play.google.com/store/apps/details?id=" + activity.getPackageName()));
} }
} }
private void openPlayStore(String url) { private void openPlayStore(String url) {
@ -62,29 +60,26 @@ public class AboutFragment extends Fragment {
intent.setData(Uri.parse(url)); intent.setData(Uri.parse(url));
intent.setPackage("com.android.vending"); intent.setPackage("com.android.vending");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.getActivity().startActivity(intent); CommonLogic.doOnNonNull(this.getActivity(), (activity) -> activity.startActivity(intent));
} }
@OnClick({R.id.button_qq_group_1, R.id.button_qq_group_2}) @OnClick({R.id.button_qq_group_1, R.id.button_qq_group_2})
void joinQQ(Button which) { void joinQQ(Button which) {
String baseUrl = "mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D"; String baseUrl = "mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D";
if (which.getId() == R.id.button_qq_group_1) { if (which.getId() == R.id.button_qq_group_1) {
CommonLogic.openUrl(this.getContext(), baseUrl + "AAflCLHiWw1haM1obu_f-CpGsETxXc6b"); CommonLogic.doOnNonNull(this.getContext(), (context) -> CommonLogic.openUrl(context, baseUrl + "AAflCLHiWw1haM1obu_f-CpGsETxXc6b"));
} else { } else {
CommonLogic.openUrl(this.getContext(), baseUrl + "kshK7BavcS2jXZ6exDvezc18ksLB8YsM"); CommonLogic.doOnNonNull(this.getContext(), (context) -> CommonLogic.openUrl(context, baseUrl + "kshK7BavcS2jXZ6exDvezc18ksLB8YsM"));
} }
} }
@OnClick(R.id.button_donation) @OnClick(R.id.button_donation)
void donation() { void donation() {
Context context = this.getContext(); CommonLogic.doOnNonNull(this.getContext(), (context) -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(context)
DialogUtils.setCurrentDialog(new MaterialDialog.Builder(context)
.title(R.string.button_donation_text) .title(R.string.button_donation_text)
.items(R.array.donation_methods) .items(R.array.donation_methods)
.itemsCallback((dialog, itemView, position, text) -> .itemsCallback((dialog, itemView, position, text) ->
CommonLogic.showAnimation(imgHeart, R.anim.heart_beat, (animation) -> { CommonLogic.showAnimation(imgHeart, R.anim.heart_beat, (animation) -> listSelectLogic(context, position))).show()));
listSelectLogic(context, position);
})).show());
} }
private void listSelectLogic(Context context, int position) { private void listSelectLogic(Context context, int position) {
@ -108,10 +103,11 @@ public class AboutFragment extends Fragment {
if (hasInstalledAlipayClient) { if (hasInstalledAlipayClient) {
if (CommonLogic.copyToClipboard(context, Constants.RED_PACKET_CODE)) { if (CommonLogic.copyToClipboard(context, Constants.RED_PACKET_CODE)) {
PackageManager packageManager = context.getPackageManager(); PackageManager packageManager = context.getPackageManager();
Intent intent = packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone"); CommonLogic.doOnNonNull(packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone"), (intent) -> {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent); context.startActivity(intent);
Toast.makeText(context, R.string.toast_redpacket_message, Toast.LENGTH_LONG).show(); Toast.makeText(context, R.string.toast_redpacket_message, Toast.LENGTH_LONG).show();
});
} }
} }
break; break;

View File

@ -14,6 +14,7 @@ import com.afollestad.materialdialogs.DialogAction;
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.logic.CommonLogic;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
import com.zane.smapiinstaller.utils.FileUtils; import com.zane.smapiinstaller.utils.FileUtils;
import com.zane.smapiinstaller.utils.JSONUtil; import com.zane.smapiinstaller.utils.JSONUtil;
@ -48,33 +49,36 @@ public class ConfigEditFragment extends Fragment {
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_config_edit, container, false); View root = inflater.inflate(R.layout.fragment_config_edit, container, false);
ButterKnife.bind(this, root); ButterKnife.bind(this, root);
editable = this.getArguments().getBoolean("editable"); CommonLogic.doOnNonNull(this.getArguments(), arguments -> {
if(!editable) { editable = arguments.getBoolean("editable");
configPath = arguments.getString("configPath");
});
if (!editable) {
editText.setKeyListener(null); editText.setKeyListener(null);
buttonConfigSave.setVisibility(View.INVISIBLE); buttonConfigSave.setVisibility(View.INVISIBLE);
buttonConfigCancel.setVisibility(View.INVISIBLE); buttonConfigCancel.setVisibility(View.INVISIBLE);
} }
configPath = this.getArguments().getString("configPath"); if (configPath != null) {
if(configPath != null) {
File file = new File(configPath); File file = new File(configPath);
if(file.exists() && file.length() < Constants.TEXT_FILE_OPEN_SIZE_LIMIT) { if (file.exists() && file.length() < Constants.TEXT_FILE_OPEN_SIZE_LIMIT) {
String fileText = FileUtils.getFileText(file); String fileText = FileUtils.getFileText(file);
if (fileText != null) { if (fileText != null) {
editText.setText(fileText); editText.setText(fileText);
} }
} } else {
else {
editText.setText(""); editText.setText("");
editText.setKeyListener(null); editText.setKeyListener(null);
DialogUtils.showConfirmDialog(root, R.string.error, this.getString(R.string.text_too_large), R.string.open_with, R.string.cancel, ((dialog, which) -> { DialogUtils.showConfirmDialog(root, R.string.error, this.getString(R.string.text_too_large), R.string.open_with, R.string.cancel, ((dialog, which) -> {
if(which == DialogAction.POSITIVE) { if (which == DialogAction.POSITIVE) {
Intent intent = new Intent("android.intent.action.VIEW"); Intent intent = new Intent("android.intent.action.VIEW");
intent.addCategory("android.intent.category.DEFAULT"); intent.addCategory("android.intent.category.DEFAULT");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(this.getContext(), BuildConfig.APPLICATION_ID + ".provider", file); CommonLogic.doOnNonNull(this.getContext(), (context -> {
intent.setDataAndType(contentUri, "text/plain"); Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file);
intent.setDataAndType(contentUri, "text/plain");
}));
} else { } else {
intent.setDataAndType(Uri.fromFile(file), "text/plain"); intent.setDataAndType(Uri.fromFile(file), "text/plain");
} }
@ -86,21 +90,23 @@ public class ConfigEditFragment extends Fragment {
} }
return root; return root;
} }
@OnClick(R.id.button_config_save) void onConfigSave() {
@OnClick(R.id.button_config_save)
void onConfigSave() {
try { try {
JSONUtil.checkJson(editText.getText().toString()); JSONUtil.checkJson(editText.getText().toString());
FileOutputStream outputStream = new FileOutputStream(configPath); FileOutputStream outputStream = new FileOutputStream(configPath);
try(OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)){ try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)) {
outputStreamWriter.write(editText.getText().toString()); outputStreamWriter.write(editText.getText().toString());
outputStreamWriter.flush(); outputStreamWriter.flush();
} }
} } catch (Exception e) {
catch (Exception e) {
DialogUtils.showAlertDialog(getView(), R.string.error, e.getLocalizedMessage()); DialogUtils.showAlertDialog(getView(), R.string.error, e.getLocalizedMessage());
} }
} }
@OnClick(R.id.button_config_cancel) void onConfigCancel() { @OnClick(R.id.button_config_cancel)
Navigation.findNavController(getView()).popBackStack(); void onConfigCancel() {
CommonLogic.doOnNonNull(getView(), view -> Navigation.findNavController(view).popBackStack());
} }
} }

View File

@ -17,6 +17,7 @@ import butterknife.OnTextChanged;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import com.zane.smapiinstaller.R; import com.zane.smapiinstaller.R;
import com.zane.smapiinstaller.logic.CommonLogic;
import com.zane.smapiinstaller.utils.DialogUtils; import com.zane.smapiinstaller.utils.DialogUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -55,7 +56,7 @@ public class ConfigFragment extends Fragment {
@OnClick(R.id.button_sort_by) @OnClick(R.id.button_sort_by)
void onSortByClick() { void onSortByClick() {
int index; int index;
switch (configViewModel.getSortBy()){ switch (configViewModel.getSortBy()) {
case "Name asc": case "Name asc":
index = 0; index = 0;
break; break;
@ -71,24 +72,27 @@ public class ConfigFragment extends Fragment {
default: default:
index = 0; index = 0;
} }
DialogUtils.setCurrentDialog(new MaterialDialog.Builder(this.getContext()).title(R.string.sort_by).items(R.array.mod_list_sort_by).itemsCallbackSingleChoice(index, (dialog, itemView, position, text) -> { CommonLogic.doOnNonNull(this.getContext(), context -> DialogUtils.setCurrentDialog(new MaterialDialog.Builder(context)
switch (position) { .title(R.string.sort_by)
case 0: .items(R.array.mod_list_sort_by)
configViewModel.switchSortBy("Name asc"); .itemsCallbackSingleChoice(index, (dialog, itemView, position, text) -> {
break; switch (position) {
case 1: case 0:
configViewModel.switchSortBy("Name desc"); configViewModel.switchSortBy("Name asc");
break; break;
case 2: case 1:
configViewModel.switchSortBy("Date asc"); configViewModel.switchSortBy("Name desc");
break; break;
case 3: case 2:
configViewModel.switchSortBy("Date desc"); configViewModel.switchSortBy("Date asc");
break; break;
default: case 3:
return false; configViewModel.switchSortBy("Date desc");
} break;
return true; default:
}).show()); return false;
}
return true;
}).show()));
} }
} }

View File

@ -39,9 +39,11 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
private List<ModManifestEntry> filteredModList; private List<ModManifestEntry> filteredModList;
private String sortBy = "Name asc"; private String sortBy = "Name asc";
public String getSortBy() { public String getSortBy() {
return sortBy; return sortBy;
} }
private final View root; private final View root;
private final List<Predicate<List<ModManifestEntry>>> onChangedListener = new ArrayList<>(); private final List<Predicate<List<ModManifestEntry>>> onChangedListener = new ArrayList<>();
@ -55,7 +57,7 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
AppConfigDao appConfigDao = app.getDaoSession().getAppConfigDao(); AppConfigDao appConfigDao = app.getDaoSession().getAppConfigDao();
Query<AppConfig> query = appConfigDao.queryBuilder().where(AppConfigDao.Properties.Name.eq(AppConfigKey.MOD_LIST_SORT_BY)).build(); Query<AppConfig> query = appConfigDao.queryBuilder().where(AppConfigDao.Properties.Name.eq(AppConfigKey.MOD_LIST_SORT_BY)).build();
AppConfig appConfig = query.unique(); AppConfig appConfig = query.unique();
if(null != appConfig) { if (null != appConfig) {
sortBy = appConfig.getValue(); sortBy = appConfig.getValue();
} }
} }
@ -64,7 +66,7 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
public void switchSortBy(String sortBy) { public void switchSortBy(String sortBy) {
MainApplication app = CommonLogic.getApplicationFromView(root); MainApplication app = CommonLogic.getApplicationFromView(root);
if(null == app) { if (null == app) {
return; return;
} }
this.sortBy = sortBy; this.sortBy = sortBy;
@ -78,35 +80,34 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
switch (sortBy) { switch (sortBy) {
case "Name asc": case "Name asc":
Collections.sort(modList, (a, b) -> a.getName().compareTo(b.getName())); Collections.sort(modList, (a, b) -> a.getName().compareTo(b.getName()));
if(filteredModList != null && filteredModList != modList) { if (filteredModList != null && filteredModList != modList) {
Collections.sort(filteredModList, (a, b) -> a.getName().compareTo(b.getName())); Collections.sort(filteredModList, (a, b) -> a.getName().compareTo(b.getName()));
} }
break; break;
case "Name desc": case "Name desc":
Collections.sort(modList, (a, b) -> b.getName().compareTo(a.getName())); Collections.sort(modList, (a, b) -> b.getName().compareTo(a.getName()));
if(filteredModList != null && filteredModList != modList) { if (filteredModList != null && filteredModList != modList) {
Collections.sort(filteredModList, (a, b) -> b.getName().compareTo(a.getName())); Collections.sort(filteredModList, (a, b) -> b.getName().compareTo(a.getName()));
} }
break; break;
case "Date asc": case "Date asc":
Collections.sort(modList, (a, b) -> a.getLastModified().compareTo(b.getLastModified())); Collections.sort(modList, (a, b) -> a.getLastModified().compareTo(b.getLastModified()));
if(filteredModList != null && filteredModList != modList) { if (filteredModList != null && filteredModList != modList) {
Collections.sort(filteredModList, (a, b) -> a.getLastModified().compareTo(b.getLastModified())); Collections.sort(filteredModList, (a, b) -> a.getLastModified().compareTo(b.getLastModified()));
} }
break; break;
case "Date desc": case "Date desc":
Collections.sort(modList, (a, b) -> b.getLastModified().compareTo(a.getLastModified())); Collections.sort(modList, (a, b) -> b.getLastModified().compareTo(a.getLastModified()));
if(filteredModList != null && filteredModList != modList) { if (filteredModList != null && filteredModList != modList) {
Collections.sort(filteredModList, (a, b) -> b.getLastModified().compareTo(a.getLastModified())); Collections.sort(filteredModList, (a, b) -> b.getLastModified().compareTo(a.getLastModified()));
} }
break; break;
default: default:
return; return;
} }
if(filteredModList != null) { if (filteredModList != null) {
emitDataChangeEvent(filteredModList); emitDataChangeEvent(filteredModList);
} } else {
else {
emitDataChangeEvent(modList); emitDataChangeEvent(modList);
} }
} }
@ -136,15 +137,17 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
} }
}).filter(Objects::nonNull).distinct().collect(Collectors.toList()); }).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (!untranslatedText.isEmpty()) { if (!untranslatedText.isEmpty()) {
TranslateUtil.translateText(untranslatedText, translator, language, (results) -> { TranslateUtil.translateText(untranslatedText, translator, language, (result) -> {
daoSession.getTranslationResultDao().insertOrReplaceInTx(results); CommonLogic.doOnNonNull(result, (results) -> {
ImmutableMap<String, TranslationResult> map = Maps.uniqueIndex(results, TranslationResult::getOrigin); daoSession.getTranslationResultDao().insertOrReplaceInTx(results);
for (ModManifestEntry mod : modList) { ImmutableMap<String, TranslationResult> map = Maps.uniqueIndex(results, TranslationResult::getOrigin);
if (map.containsKey(mod.getDescription())) { for (ModManifestEntry mod : modList) {
mod.setTranslatedDescription(map.get(mod.getDescription()).getTranslation()); if (map.containsKey(mod.getDescription())) {
mod.setTranslatedDescription(map.get(mod.getDescription()).getTranslation());
}
} }
} emitDataChangeEvent(modList);
emitDataChangeEvent(modList); });
return true; return true;
}); });
} }

View File

@ -8,7 +8,6 @@ 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.base.Predicate;
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.ModManifestEntry; import com.zane.smapiinstaller.entity.ModManifestEntry;
@ -29,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import java9.util.function.Predicate;
/** /**
* @author Zane * @author Zane
@ -104,7 +104,7 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
public List<Integer> removeAll(Predicate<ModManifestEntry> predicate) { public List<Integer> removeAll(Predicate<ModManifestEntry> predicate) {
List<Integer> deletedId = new ArrayList<>(); List<Integer> deletedId = new ArrayList<>();
for (int i = modList.size() - 1; i >= 0; i--) { for (int i = modList.size() - 1; i >= 0; i--) {
if (predicate.apply(modList.get(i))) { if (predicate.test(modList.get(i))) {
modList.remove(i); modList.remove(i);
deletedId.add(i); deletedId.add(i);
} }
@ -151,7 +151,7 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
public Integer findFirst(Predicate<ModManifestEntry> predicate) { public Integer findFirst(Predicate<ModManifestEntry> predicate) {
for (int i = 0; i < modList.size(); i++) { for (int i = 0; i < modList.size(); i++) {
if (predicate.apply(modList.get(i))) { if (predicate.test(modList.get(i))) {
return i; return i;
} }
} }

View File

@ -50,18 +50,20 @@ public class HelpFragment extends Fragment {
return root; return root;
} }
@OnClick(R.id.button_compat) void compat() { @OnClick(R.id.button_compat) void compat() {
CommonLogic.openUrl(this.getContext(), "https://smapi.io/mods"); CommonLogic.doOnNonNull(this.getContext(), context -> CommonLogic.openUrl(context, "https://smapi.io/mods"));
} }
@OnClick(R.id.button_nexus) void nexus() { @OnClick(R.id.button_nexus) void nexus() {
CommonLogic.openUrl(this.getContext(), "https://www.nexusmods.com/stardewvalley/mods/"); CommonLogic.doOnNonNull(this.getContext(), context -> CommonLogic.openUrl(context, "https://www.nexusmods.com/stardewvalley/mods/"));
} }
@OnClick({R.id.button_logs}) void showLog() { @OnClick({R.id.button_logs}) void showLog() {
NavController controller = Navigation.findNavController(this.getView()); CommonLogic.doOnNonNull(this.getView(), view -> {
File logFile = new File(Environment.getExternalStorageDirectory(), Constants.LOG_PATH); NavController controller = Navigation.findNavController(view);
if(logFile.exists()) { File logFile = new File(Environment.getExternalStorageDirectory(), Constants.LOG_PATH);
HelpFragmentDirections.ActionNavHelpToConfigEditFragment action = HelpFragmentDirections.actionNavHelpToConfigEditFragment(logFile.getAbsolutePath()); if(logFile.exists()) {
action.setEditable(false); HelpFragmentDirections.ActionNavHelpToConfigEditFragment action = HelpFragmentDirections.actionNavHelpToConfigEditFragment(logFile.getAbsolutePath());
controller.navigate(action); action.setEditable(false);
} controller.navigate(action);
}
});
} }
} }

View File

@ -0,0 +1,48 @@
package com.zane.smapiinstaller.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.lzy.okgo.callback.AbsCallback;
import com.lzy.okgo.request.base.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public abstract class JsonCallback<T> extends AbsCallback<T> {
private TypeReference<T> type;
private Class<T> clazz;
public JsonCallback(TypeReference<T> type) {
this.type = type;
}
public JsonCallback(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public void onStart(Request<T, ? extends Request> request) {
super.onStart(request);
}
/**
* 该方法是子线程处理不能做ui相关的工作
* 主要作用是解析网络返回的 response 对象,生产onSuccess回调中需要的数据对象
* 这里的解析工作不同的业务逻辑基本都不一样,所以需要自己实现,以下给出的时模板代码,实际使用根据需要修改
*/
@Override
public T convertResponse(Response response) throws Throwable {
ResponseBody body = response.body();
if (body == null) {
return null;
}
T data = null;
if (type != null) {
data = JSONUtil.fromJson(body.string(), type);
}
if (clazz != null) {
data = JSONUtil.fromJson(body.string(), clazz);
}
return data;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/toolbar_update_check"
android:orderInCategory="99"
android:icon="@drawable/update_check"
app:showAsAction="always"
android:title="" />
<group <group
android:checkableBehavior="all"> android:checkableBehavior="all">
<item <item