parent
5743e50e04
commit
e69565d5e3
|
@ -1,10 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.zane.smapiinstaller.entity.DaoSession;
|
|||
import com.zane.smapiinstaller.entity.FrameworkConfig;
|
||||
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.TranslateUtil;
|
||||
|
||||
|
@ -168,6 +169,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
case R.id.settings_translation_service:
|
||||
selectTranslateServiceLogic();
|
||||
return true;
|
||||
case R.id.toolbar_update_check:
|
||||
updateCheckLogic();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
@ -270,6 +274,11 @@ public class MainActivity extends AppCompatActivity {
|
|||
}).show());
|
||||
}
|
||||
|
||||
private void updateCheckLogic() {
|
||||
ModAssetsManager modAssetsManager = new ModAssetsManager(toolbar);
|
||||
modAssetsManager.checkModUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
|
||||
|
|
|
@ -8,7 +8,6 @@ import com.lzy.okgo.OkGo;
|
|||
import com.zane.smapiinstaller.entity.DaoMaster;
|
||||
import com.zane.smapiinstaller.entity.DaoSession;
|
||||
import com.zane.smapiinstaller.utils.DbOpenHelper;
|
||||
import com.zane.smapiinstaller.utils.GzipRequestInterceptor;
|
||||
|
||||
import org.greenrobot.greendao.database.Database;
|
||||
|
||||
|
@ -27,7 +26,7 @@ public class MainApplication extends Application {
|
|||
super.onCreate();
|
||||
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
||||
//开启Gzip压缩
|
||||
.addInterceptor(new GzipRequestInterceptor())
|
||||
// .addInterceptor(new GzipRequestInterceptor())
|
||||
.build();
|
||||
OkGo.getInstance().setOkHttpClient(okHttpClient).init(this);
|
||||
LanguagesManager.init(this);
|
||||
|
|
|
@ -69,4 +69,9 @@ public class Constants {
|
|||
* 平台
|
||||
*/
|
||||
public static final String PLATFORM = "Android";
|
||||
|
||||
/**
|
||||
* SMAPI更新服务
|
||||
*/
|
||||
public static final String UPDATE_CHECK_SERVICE_URL = "https://smapi.io/api/v" + SMAPI_VERSION + "/mods";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -189,6 +189,9 @@ public class ApkPatcher {
|
|||
AtomicReference<String> packageName = new AtomicReference<>();
|
||||
AtomicLong versionCode = new AtomicLong();
|
||||
Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> {
|
||||
if(attr == null) {
|
||||
return true;
|
||||
}
|
||||
if (attr.type == NodeVisitor.TYPE_STRING) {
|
||||
String strObj = (String) attr.obj;
|
||||
switch (attr.name) {
|
||||
|
@ -227,6 +230,9 @@ public class ApkPatcher {
|
|||
try {
|
||||
byte[] modifyManifest = CommonLogic.modifyManifest(bytes, processLogic);
|
||||
Iterables.removeIf(manifests, manifest -> {
|
||||
if(manifest == null) {
|
||||
return true;
|
||||
}
|
||||
if (versionCode.get() < manifest.getMinBuildCode()) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -42,11 +42,13 @@ import pxb.android.axml.NodeVisitor;
|
|||
|
||||
/**
|
||||
* 通用逻辑
|
||||
*
|
||||
* @author Zane
|
||||
*/
|
||||
public class CommonLogic {
|
||||
/**
|
||||
* 从View获取所属Activity
|
||||
*
|
||||
* @param view context容器
|
||||
* @return Activity
|
||||
*/
|
||||
|
@ -65,19 +67,34 @@ public class CommonLogic {
|
|||
|
||||
/**
|
||||
* 从一个View获取Application
|
||||
*
|
||||
* @param view 控件
|
||||
* @return Application
|
||||
*/
|
||||
public static MainApplication getApplicationFromView(View view) {
|
||||
Activity activity = getActivityFromView(view);
|
||||
if(null != activity) {
|
||||
if (null != activity) {
|
||||
return (MainApplication) activity.getApplication();
|
||||
}
|
||||
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
|
||||
*
|
||||
* @param context context
|
||||
* @param url 目标URL
|
||||
*/
|
||||
|
@ -87,22 +104,23 @@ public class CommonLogic {
|
|||
intent.setData(Uri.parse(url));
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
catch (ActivityNotFoundException ignored){
|
||||
} catch (ActivityNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param copyStr 文本
|
||||
* @return 是否复制成功
|
||||
*/
|
||||
public static boolean copyToClipboard(Context context, String copyStr) {
|
||||
try {
|
||||
ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData mClipData = ClipData.newPlainText("Label", copyStr);
|
||||
cm.setPrimaryClip(mClipData);
|
||||
CommonLogic.doOnNonNull(context.getSystemService(Context.CLIPBOARD_SERVICE), cm -> {
|
||||
ClipData mClipData = ClipData.newPlainText("Label", copyStr);
|
||||
((ClipboardManager) cm).setPrimaryClip(mClipData);
|
||||
});
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
|
@ -111,6 +129,7 @@ public class CommonLogic {
|
|||
|
||||
/**
|
||||
* 扫描全部兼容包
|
||||
*
|
||||
* @param context context
|
||||
* @return 兼容包列表
|
||||
*/
|
||||
|
@ -130,10 +149,9 @@ public class CommonLogic {
|
|||
}
|
||||
}
|
||||
Collections.sort(apkFilesManifests, (a, b) -> {
|
||||
if(a.getTargetPackageName() != null && b.getTargetPackageName() == null) {
|
||||
if (a.getTargetPackageName() != null && b.getTargetPackageName() == null) {
|
||||
return -1;
|
||||
}
|
||||
else if(b.getTargetPackageName() != null){
|
||||
} else if (b.getTargetPackageName() != null) {
|
||||
return Long.compare(b.getMinBuildCode(), a.getMinBuildCode());
|
||||
}
|
||||
return 1;
|
||||
|
@ -143,13 +161,15 @@ public class CommonLogic {
|
|||
|
||||
/**
|
||||
* 提取SMAPI环境文件到内部存储对应位置
|
||||
* @param context context
|
||||
* @param apkPath 安装包路径
|
||||
*
|
||||
* @param context context
|
||||
* @param apkPath 安装包路径
|
||||
* @param checkMode 是否为校验模式
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
@ -199,6 +219,7 @@ public class CommonLogic {
|
|||
|
||||
/**
|
||||
* 修改AndroidManifest.xml文件
|
||||
*
|
||||
* @param bytes AndroidManifest.xml文件字符数组
|
||||
* @param processLogic 处理逻辑
|
||||
* @return 修改后的AndroidManifest.xml文件字符数组
|
||||
|
|
|
@ -24,10 +24,11 @@ public class GameLauncher {
|
|||
}
|
||||
|
||||
/**
|
||||
* 启动逻辑
|
||||
* 检查已安装MOD版本游戏
|
||||
* @param context 上下文
|
||||
* @return 软件包信息
|
||||
*/
|
||||
public void launch() {
|
||||
Activity context = CommonLogic.getActivityFromView(root);
|
||||
public static PackageInfo getGamePackageInfo(Activity context) {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
try {
|
||||
PackageInfo packageInfo;
|
||||
|
@ -36,20 +37,35 @@ public class GameLauncher {
|
|||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
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)) {
|
||||
DialogUtils.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair);
|
||||
return;
|
||||
}
|
||||
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
|
||||
PackageInfo finalPackageInfo = packageInfo;
|
||||
modAssetsManager.checkModEnvironment((isConfirm) -> {
|
||||
if(isConfirm) {
|
||||
Intent intent = packageManager.getLaunchIntentForPackage(finalPackageInfo.packageName);
|
||||
Intent intent = packageManager.getLaunchIntentForPackage(packageInfo.packageName);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
});
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
DialogUtils.showAlertDialog(root, R.string.error, R.string.error_smapi_not_installed);
|
||||
} catch (Exception e) {
|
||||
Crashes.trackError(e);
|
||||
DialogUtils.showAlertDialog(root, R.string.error, e.getLocalizedMessage());
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.zane.smapiinstaller.logic;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
@ -8,17 +9,23 @@ import android.view.View;
|
|||
import com.afollestad.materialdialogs.DialogAction;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimaps;
|
||||
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.constant.Constants;
|
||||
import com.zane.smapiinstaller.dto.ModUpdateCheckRequestDto;
|
||||
import com.zane.smapiinstaller.dto.ModUpdateCheckResponseDto;
|
||||
import com.zane.smapiinstaller.entity.ModManifestEntry;
|
||||
import com.zane.smapiinstaller.utils.DialogUtils;
|
||||
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 org.apache.commons.lang3.StringUtils;
|
||||
|
@ -30,12 +37,12 @@ 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.Predicate;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
/**
|
||||
* Mod资源管理器
|
||||
*/
|
||||
|
@ -68,7 +75,7 @@ public class ModAssetsManager {
|
|||
foundManifest = true;
|
||||
if (manifest != null) {
|
||||
manifest.setAssetPath(file.getParentFile().getAbsolutePath());
|
||||
if (filter.apply(manifest)) {
|
||||
if (filter.test(manifest)) {
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +162,7 @@ public class ModAssetsManager {
|
|||
if (installedMods.size() > 1) {
|
||||
DialogUtils.showAlertDialog(root, R.string.error,
|
||||
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;
|
||||
} else if (installedMods.size() == 0) {
|
||||
installedMods = installedModMap.get(mod.getUniqueID().replace("ZaneYork.CustomLocalization", "SMAPI.CustomLocalization"));
|
||||
|
@ -212,7 +219,7 @@ public class ModAssetsManager {
|
|||
for (String key : installedModMap.keySet()) {
|
||||
ImmutableList<ModManifestEntry> installedMods = installedModMap.get(key);
|
||||
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()) {
|
||||
|
@ -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) {
|
||||
if (mod.getDependencies() != null) {
|
||||
List<ModManifestEntry> unsatisfiedDependencies = StreamSupport.stream(mod.getDependencies())
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment;
|
|||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import java9.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Zane
|
||||
|
@ -44,17 +43,16 @@ public class AboutFragment extends Fragment {
|
|||
|
||||
@OnClick(R.id.button_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)
|
||||
void gplay() {
|
||||
try {
|
||||
this.openPlayStore("market://details?id=" + this.getActivity().getPackageName());
|
||||
CommonLogic.doOnNonNull(this.getActivity(), (activity) -> this.openPlayStore("market://details?id=" + activity.getPackageName()));
|
||||
} 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) {
|
||||
|
@ -62,29 +60,26 @@ public class AboutFragment extends Fragment {
|
|||
intent.setData(Uri.parse(url));
|
||||
intent.setPackage("com.android.vending");
|
||||
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})
|
||||
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";
|
||||
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 {
|
||||
CommonLogic.openUrl(this.getContext(), baseUrl + "kshK7BavcS2jXZ6exDvezc18ksLB8YsM");
|
||||
CommonLogic.doOnNonNull(this.getContext(), (context) -> CommonLogic.openUrl(context, baseUrl + "kshK7BavcS2jXZ6exDvezc18ksLB8YsM"));
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.button_donation)
|
||||
void donation() {
|
||||
Context context = this.getContext();
|
||||
DialogUtils.setCurrentDialog(new MaterialDialog.Builder(context)
|
||||
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());
|
||||
CommonLogic.showAnimation(imgHeart, R.anim.heart_beat, (animation) -> listSelectLogic(context, position))).show()));
|
||||
}
|
||||
|
||||
private void listSelectLogic(Context context, int position) {
|
||||
|
@ -108,10 +103,11 @@ public class AboutFragment extends Fragment {
|
|||
if (hasInstalledAlipayClient) {
|
||||
if (CommonLogic.copyToClipboard(context, Constants.RED_PACKET_CODE)) {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
Intent intent = packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
Toast.makeText(context, R.string.toast_redpacket_message, Toast.LENGTH_LONG).show();
|
||||
CommonLogic.doOnNonNull(packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone"), (intent) -> {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
Toast.makeText(context, R.string.toast_redpacket_message, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -14,6 +14,7 @@ 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.logic.CommonLogic;
|
||||
import com.zane.smapiinstaller.utils.DialogUtils;
|
||||
import com.zane.smapiinstaller.utils.FileUtils;
|
||||
import com.zane.smapiinstaller.utils.JSONUtil;
|
||||
|
@ -48,33 +49,36 @@ public class ConfigEditFragment extends Fragment {
|
|||
ViewGroup container, Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_config_edit, container, false);
|
||||
ButterKnife.bind(this, root);
|
||||
editable = this.getArguments().getBoolean("editable");
|
||||
if(!editable) {
|
||||
CommonLogic.doOnNonNull(this.getArguments(), arguments -> {
|
||||
editable = arguments.getBoolean("editable");
|
||||
configPath = arguments.getString("configPath");
|
||||
});
|
||||
if (!editable) {
|
||||
editText.setKeyListener(null);
|
||||
buttonConfigSave.setVisibility(View.INVISIBLE);
|
||||
buttonConfigCancel.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
configPath = this.getArguments().getString("configPath");
|
||||
if(configPath != null) {
|
||||
if (configPath != null) {
|
||||
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);
|
||||
if (fileText != null) {
|
||||
editText.setText(fileText);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
editText.setText("");
|
||||
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) -> {
|
||||
if(which == DialogAction.POSITIVE) {
|
||||
if (which == DialogAction.POSITIVE) {
|
||||
Intent intent = new Intent("android.intent.action.VIEW");
|
||||
intent.addCategory("android.intent.category.DEFAULT");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
Uri contentUri = FileProvider.getUriForFile(this.getContext(), BuildConfig.APPLICATION_ID + ".provider", file);
|
||||
intent.setDataAndType(contentUri, "text/plain");
|
||||
CommonLogic.doOnNonNull(this.getContext(), (context -> {
|
||||
Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file);
|
||||
intent.setDataAndType(contentUri, "text/plain");
|
||||
}));
|
||||
} else {
|
||||
intent.setDataAndType(Uri.fromFile(file), "text/plain");
|
||||
}
|
||||
|
@ -86,21 +90,23 @@ public class ConfigEditFragment extends Fragment {
|
|||
}
|
||||
return root;
|
||||
}
|
||||
@OnClick(R.id.button_config_save) void onConfigSave() {
|
||||
|
||||
@OnClick(R.id.button_config_save)
|
||||
void onConfigSave() {
|
||||
try {
|
||||
JSONUtil.checkJson(editText.getText().toString());
|
||||
FileOutputStream outputStream = new FileOutputStream(configPath);
|
||||
try(OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)){
|
||||
try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)) {
|
||||
outputStreamWriter.write(editText.getText().toString());
|
||||
outputStreamWriter.flush();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
DialogUtils.showAlertDialog(getView(), R.string.error, e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.button_config_cancel) void onConfigCancel() {
|
||||
Navigation.findNavController(getView()).popBackStack();
|
||||
@OnClick(R.id.button_config_cancel)
|
||||
void onConfigCancel() {
|
||||
CommonLogic.doOnNonNull(getView(), view -> Navigation.findNavController(view).popBackStack());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ 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;
|
||||
|
@ -55,7 +56,7 @@ public class ConfigFragment extends Fragment {
|
|||
@OnClick(R.id.button_sort_by)
|
||||
void onSortByClick() {
|
||||
int index;
|
||||
switch (configViewModel.getSortBy()){
|
||||
switch (configViewModel.getSortBy()) {
|
||||
case "Name asc":
|
||||
index = 0;
|
||||
break;
|
||||
|
@ -71,24 +72,27 @@ public class ConfigFragment extends Fragment {
|
|||
default:
|
||||
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) -> {
|
||||
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());
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,11 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
|
|||
private List<ModManifestEntry> filteredModList;
|
||||
|
||||
private String sortBy = "Name asc";
|
||||
|
||||
public String getSortBy() {
|
||||
return sortBy;
|
||||
}
|
||||
|
||||
private final View root;
|
||||
|
||||
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();
|
||||
Query<AppConfig> query = appConfigDao.queryBuilder().where(AppConfigDao.Properties.Name.eq(AppConfigKey.MOD_LIST_SORT_BY)).build();
|
||||
AppConfig appConfig = query.unique();
|
||||
if(null != appConfig) {
|
||||
if (null != appConfig) {
|
||||
sortBy = appConfig.getValue();
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +66,7 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
|
|||
|
||||
public void switchSortBy(String sortBy) {
|
||||
MainApplication app = CommonLogic.getApplicationFromView(root);
|
||||
if(null == app) {
|
||||
if (null == app) {
|
||||
return;
|
||||
}
|
||||
this.sortBy = sortBy;
|
||||
|
@ -78,35 +80,34 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
|
|||
switch (sortBy) {
|
||||
case "Name asc":
|
||||
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()));
|
||||
}
|
||||
break;
|
||||
case "Name desc":
|
||||
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()));
|
||||
}
|
||||
break;
|
||||
case "Date asc":
|
||||
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()));
|
||||
}
|
||||
break;
|
||||
case "Date desc":
|
||||
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()));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if(filteredModList != null) {
|
||||
if (filteredModList != null) {
|
||||
emitDataChangeEvent(filteredModList);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
emitDataChangeEvent(modList);
|
||||
}
|
||||
}
|
||||
|
@ -136,15 +137,17 @@ class ConfigViewModel extends ViewModel implements ListenableObject<List<ModMani
|
|||
}
|
||||
}).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (!untranslatedText.isEmpty()) {
|
||||
TranslateUtil.translateText(untranslatedText, translator, language, (results) -> {
|
||||
daoSession.getTranslationResultDao().insertOrReplaceInTx(results);
|
||||
ImmutableMap<String, TranslationResult> map = Maps.uniqueIndex(results, TranslationResult::getOrigin);
|
||||
for (ModManifestEntry mod : modList) {
|
||||
if (map.containsKey(mod.getDescription())) {
|
||||
mod.setTranslatedDescription(map.get(mod.getDescription()).getTranslation());
|
||||
TranslateUtil.translateText(untranslatedText, translator, language, (result) -> {
|
||||
CommonLogic.doOnNonNull(result, (results) -> {
|
||||
daoSession.getTranslationResultDao().insertOrReplaceInTx(results);
|
||||
ImmutableMap<String, TranslationResult> map = Maps.uniqueIndex(results, TranslationResult::getOrigin);
|
||||
for (ModManifestEntry mod : modList) {
|
||||
if (map.containsKey(mod.getDescription())) {
|
||||
mod.setTranslatedDescription(map.get(mod.getDescription()).getTranslation());
|
||||
}
|
||||
}
|
||||
}
|
||||
emitDataChangeEvent(modList);
|
||||
emitDataChangeEvent(modList);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.widget.Button;
|
|||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.DialogAction;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.constant.Constants;
|
||||
import com.zane.smapiinstaller.entity.ModManifestEntry;
|
||||
|
@ -29,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import java9.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @author Zane
|
||||
|
@ -104,7 +104,7 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
|
|||
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))) {
|
||||
if (predicate.test(modList.get(i))) {
|
||||
modList.remove(i);
|
||||
deletedId.add(i);
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.
|
|||
|
||||
public Integer findFirst(Predicate<ModManifestEntry> predicate) {
|
||||
for (int i = 0; i < modList.size(); i++) {
|
||||
if (predicate.apply(modList.get(i))) {
|
||||
if (predicate.test(modList.get(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,18 +50,20 @@ public class HelpFragment extends Fragment {
|
|||
return root;
|
||||
}
|
||||
@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() {
|
||||
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() {
|
||||
NavController controller = Navigation.findNavController(this.getView());
|
||||
File logFile = new File(Environment.getExternalStorageDirectory(), Constants.LOG_PATH);
|
||||
if(logFile.exists()) {
|
||||
HelpFragmentDirections.ActionNavHelpToConfigEditFragment action = HelpFragmentDirections.actionNavHelpToConfigEditFragment(logFile.getAbsolutePath());
|
||||
action.setEditable(false);
|
||||
controller.navigate(action);
|
||||
}
|
||||
CommonLogic.doOnNonNull(this.getView(), view -> {
|
||||
NavController controller = Navigation.findNavController(view);
|
||||
File logFile = new File(Environment.getExternalStorageDirectory(), Constants.LOG_PATH);
|
||||
if(logFile.exists()) {
|
||||
HelpFragmentDirections.ActionNavHelpToConfigEditFragment action = HelpFragmentDirections.actionNavHelpToConfigEditFragment(logFile.getAbsolutePath());
|
||||
action.setEditable(false);
|
||||
controller.navigate(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -1,6 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:checkableBehavior="all">
|
||||
<item
|
||||
|
|
Loading…
Reference in New Issue