增加上传和更新页面

This commit is contained in:
2025-04-09 23:02:52 +08:00
parent 91b3040eed
commit 64f2de69e3
28 changed files with 1806 additions and 23 deletions

View File

@@ -1,22 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.mir4updater">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DELETE_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".base.BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".base.BaseApplication"
android:theme="@style/AppTheme">
<activity android:name=".activity.MainActivity">
<activity android:name=".activity.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.update.UpdateActivity"
android:launchMode="singleTask" />
<activity
android:name=".activity.upload.UploadActivity"
android:launchMode="singleTask" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,505 @@
package com.example.mir4updater.activity.main;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.IntentSender;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import com.example.mir4updater.R;
import com.example.mir4updater.activity.update.UpdateActivity;
import com.example.mir4updater.activity.upload.UploadActivity;
import com.example.mir4updater.base.mvvm.BaseMvvmActivity;
import com.example.mir4updater.bean.FileItem;
import com.example.mir4updater.config.CommonConfig;
import com.example.mir4updater.databinding.ActivityMainBinding;
import com.example.mir4updater.utils.HashUtils;
import com.example.mir4updater.utils.PermissionUtils;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.android.play.core.appupdate.AppUpdateInfo;
import com.google.android.play.core.appupdate.AppUpdateManager;
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.google.android.play.core.install.model.AppUpdateType;
import com.google.android.play.core.install.model.UpdateAvailability;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.Toaster;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
private static final String TAG = "MainActivity";
private static final int APP_UPDATE_REQUEST_CODE = 7897;
private static final int REQUEST_CODE = 1234;
private static final int ANDROID_REQUEST_CODE = 9819;
private static final int REQUEST_CODE_OPEN_DIR = 7890;
//google play 更新
// private AppUpdateManager mAppUpdateManager;
@Override
public boolean setNightMode() {
return true;
}
@Override
public boolean setfitWindow() {
return true;
}
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
@Override
protected void initDataBinding() {
mViewModel.setCtx(this);
mViewModel.setLifecycle(getLifecycleSubject());
mViewModel.setVDBinding(mViewDataBinding);
mViewDataBinding.setClick(new BtnClick());
}
@Override
protected void initView() {
}
@Override
protected void initData() {
// mAppUpdateManager = AppUpdateManagerFactory.create(this);
// Task<AppUpdateInfo> appUpdateInfoTask = mAppUpdateManager.getAppUpdateInfo();
// appUpdateInfoTask.addOnSuccessListener(new OnSuccessListener<AppUpdateInfo>() {
// @Override
// public void onSuccess(AppUpdateInfo appUpdateInfo) {
// if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
// appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
// try {
// mAppUpdateManager.startUpdateFlowForResult(appUpdateInfo, AppUpdateType.IMMEDIATE, MainActivity.this, APP_UPDATE_REQUEST_CODE);
// } catch (IntentSender.SendIntentException e) {
// e.printStackTrace();
// }
// }
//
// }
// });
// appUpdateInfoTask.addOnFailureListener(new OnFailureListener() {
// @Override
// public void onFailure(@NonNull Exception e) {
// Log.e(TAG, "onFailure: " + e.getMessage());
// }
// });
}
@Override
protected void onResume() {
super.onResume();
// getLocationPermission();
checkPermission();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.e(TAG, "onActivityResult: " + requestCode);
if (requestCode == APP_UPDATE_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
// 处理更新失败的情况
}
} else if (requestCode == REQUEST_CODE) {
} else if (requestCode == ANDROID_REQUEST_CODE) {
readFileList();
if (data != null) {
Uri treeUri = data.getData();
saveUriPermission(treeUri); // 保存权限
}
} else if (requestCode == REQUEST_CODE_OPEN_DIR) {
if (resultCode == RESULT_OK) {
if (data != null) {
Uri treeUri = data.getData();
Log.e(TAG, "onActivityResult: " + treeUri);
DocumentFile documentFile = DocumentFile.fromTreeUri(this, treeUri);
startTraversal(documentFile);
saveUriPermission(treeUri); // 保存权限
listFiles(treeUri); // 遍历文件
// traverseDirectoryInternal(treeUri, 0);
}
}
}
}
private void checkPermission() {
mViewDataBinding.setStoragePermission(XXPermissions.isGranted(this, PermissionUtils.Location_Permission));
mViewDataBinding.setAppStoragePermission(PermissionUtils.checkAppData(MainActivity.this));
}
// 在Activity中启动遍历
private List<FileItem> fileList = new ArrayList<>();
private void startTraversal(DocumentFile root) {
fileList.clear();
// 遍历完成后更新UI例如RecyclerView
traverseDocumentFile(root);
}
// 递归遍历DocumentFile
// 递归遍历并填充列表
private void traverseDocumentFile(DocumentFile documentFile, List<FileItem> resultList, String path) {
if (documentFile == null || !documentFile.exists()) return;
DocumentFile[] files = documentFile.listFiles();
if (files == null) return;
for (DocumentFile file : files) {
if (!file.isDirectory()) {
Log.i(TAG, "traverseDocumentFile: " + getFileMd5(file.getUri()));
}
// 创建条目并加入列表
FileItem item = new FileItem(
file.getName(),
file.isDirectory(),
file.length(),
file.getUri(),
path
);
resultList.add(item);
// 如果是目录则递归遍历
if (file.isDirectory()) {
traverseDocumentFile(file, resultList, path + file.getName() + "/");
}
}
}
private void traverseDocumentFile(DocumentFile root) {
Observable.create(new ObservableOnSubscribe<List<FileItem>>() {
@Override
public void subscribe(@io.reactivex.rxjava3.annotations.NonNull ObservableEmitter<List<FileItem>> emitter) throws Throwable {
fileList.clear();
traverseDocumentFile(root, fileList, "/");
emitter.onNext(fileList);
}
}).compose(RxLifecycle.bindUntilEvent(getLifecycleSubject(), ActivityEvent.DESTROY))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<FileItem>>() {
@Override
public void onSubscribe(@io.reactivex.rxjava3.annotations.NonNull Disposable d) {
Log.e("traverseDocumentFile", "onSubscribe: ");
}
@Override
public void onNext(@io.reactivex.rxjava3.annotations.NonNull List<FileItem> fileItems) {
Log.e("traverseDocumentFile", "onNext: " + fileItems);
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Log.e("traverseDocumentFile", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("traverseDocumentFile", "onComplete: ");
}
});
}
public String getFileMd5(Uri uri) {
InputStream inputStream = null;
try {
inputStream = getContentResolver().openInputStream(uri);
if (inputStream != null) {
return HashUtils.calculateMd5(inputStream);
}
} catch (IOException | NoSuchAlgorithmException e) {
Log.e("FileHashUtils", "Error calculating MD5", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e("FileHashUtils", "Error closing stream", e);
}
}
}
return null;
}
private void requestDirectoryAccess() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
);
// 设置初始目录为 Android/data/com.xxx.yyy
Uri initialUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary:Android%2Fdata%2Fcom.xxx.yyy");
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri);
startActivityForResult(intent, REQUEST_CODE_OPEN_DIR);
}
private void saveUriPermission(Uri uri) {
// 持久化 URI 权限
getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
// 保存到 SharedPreferences可选
getSharedPreferences("MyPrefs", MODE_PRIVATE)
.edit()
.putString("saved_uri", uri.toString())
.apply();
Log.e(TAG, "saveUriPermission: " + uri.toString());
}
private void listFiles(Uri treeUri) {
ContentResolver resolver = getContentResolver();
// 构建子文件 URI
String documentId = DocumentsContract.getTreeDocumentId(treeUri);
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId);
String[] projection = new String[]{
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE
};
try (Cursor cursor = resolver.query(
childrenUri,
projection,
null,
null,
null
)) {
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(0);
String mimeType = cursor.getString(1);
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
Log.e("DIR", "Directory: " + name);
} else {
Log.e("FILE", "File: " + name);
}
}
}
} catch (SecurityException e) {
Log.e("ERROR", "权限不足: " + e.getMessage());
}
}
private void traverseDirectoryInternal(Uri treeUri, int depth) {
ContentResolver resolver = getContentResolver();
String documentId = DocumentsContract.getTreeDocumentId(treeUri);
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId);
String[] projection = new String[]{
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DOCUMENT_ID
};
try (Cursor cursor = resolver.query(childrenUri, projection, null, null, null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(0);
String mimeType = cursor.getString(1);
String childDocId = cursor.getString(2);
// 缩进显示层级
StringBuilder indent = new StringBuilder();
for (int i = 0; i < depth; i++) indent.append(" ");
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
// 目录:递归遍历
Log.e("DIR", indent + "├─ " + name);
Uri childUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocId);
traverseDirectoryInternal(childUri, depth + 1);
} else {
// 文件
Log.e("FILE", indent + "├─ " + name);
}
}
}
} catch (SecurityException e) {
Log.e("ERROR", "权限不足: " + e.getMessage());
}
}
private void readFileContent(Uri fileUri) {
ContentResolver resolver = getContentResolver();
try (InputStream inputStream = resolver.openInputStream(fileUri);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
Log.d("FILE_CONTENT", stringBuilder.toString());
} catch (Exception e) {
Log.e("ERROR", "读取文件失败: " + e.getMessage());
}
}
private void readFileList() {
Uri treeUri = Uri.parse(CommonConfig.MIR4_CONTENT_PATH);
DocumentFile documentFile = DocumentFile.fromTreeUri(this, treeUri);
if (documentFile != null) {
Log.e(TAG, "onActivityResult: canRead = " + documentFile.canRead());
Log.e(TAG, "onActivityResult: " + Arrays.toString(documentFile.listFiles()));
}
}
private void getFile() {
File file = new File("/sdcard/Android/data/com.wemade.mir4global/files/");
Log.e(TAG, "getFile: " + Arrays.toString(file.list()));
}
private void getPermssions() {
// 检查是否有存储权限
// boolean granted = Environment.isExternalStorageManager();
// Log.e(TAG, "getFile: granted = " + granted);
// if (granted) {
// // 已授权,可访问所有文件
// } else {
// // 在activity中请求存储权限
// Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
// .setData(Uri.parse("package:" + getPackageName()));
// startActivityForResult(intent, 0);
// }
// //把需要授权的目录转为Uri
// Uri uri = UriUtils.getUriFromFile(MainActivity.this, "/storage/emulated/0/Android/data/com.wemade.mir4global/");
// Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
// | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
// //这里的第二个参数是标志的意思,用来判断是谁把结果回调过来的。
// startActivityForResult(intent, 1001);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 请求Android/data目录的权限Android/obb目录则把data替换成obb即可。
Uri treeUri = Uri.parse(CommonConfig.MIR4_CONTENT_PATH);
DocumentFile documentFile = DocumentFile.fromTreeUri(this, treeUri);
if (documentFile != null) {
if (documentFile.canRead()) {
readFileList();
} else {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.getUri());
startActivityForResult(intent, ANDROID_REQUEST_CODE);
}
} else {
}
}
// Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// startActivityForResult(intent, REQUEST_CODE);
}
private void getLocationPermission() {
XXPermissions.with(this)
// 申请单个权限
.permission(PermissionUtils.Location_Permission)
// 申请多个权限
// .permission(Permission.Group.STORAGE)
// 设置权限请求拦截器(局部设置)
//.interceptor(new PermissionInterceptor())
// 设置不触发错误检测机制(局部设置)
//.unchecked()
.request(new OnPermissionCallback() {
@Override
public void onGranted(@NonNull List<String> permissions, boolean allGranted) {
Log.e(TAG, "onGranted: permissions = " + permissions + " allGranted = " + allGranted);
if (!allGranted) {
// Toaster.show("获取部分权限成功,但部分权限未正常授予");
return;
}
Log.e(TAG, "onGranted: 获取权限成功");
// getFile();
// getPermssions();
// requestDirectoryAccess();
}
@Override
public void onDenied(@NonNull List<String> permissions, boolean doNotAskAgain) {
Log.e(TAG, "onDenied: permissions = " + permissions + " doNotAskAgain = " + doNotAskAgain);
if (doNotAskAgain) {
Toaster.show("被永久拒绝授权,请手动授予权限");
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(MainActivity.this, permissions);
} else {
Log.e(TAG, "onGranted: 获取权限失败");
}
}
});
}
public class BtnClick {
public void upload(View view) {
startActivity(new Intent(MainActivity.this, UploadActivity.class));
}
public void update(View view) {
startActivity(new Intent(MainActivity.this, UpdateActivity.class));
}
public void getStoragePermission(View view) {
getLocationPermission();
}
public void getAppStoragePermission(View view) {
getPermssions();
}
}
}

View File

@@ -1,4 +1,4 @@
package com.example.mir4updater.activity;
package com.example.mir4updater.activity.main;
import com.example.mir4updater.base.mvvm.BaseViewModel;
import com.example.mir4updater.databinding.ActivityMainBinding;

View File

@@ -1,14 +1,24 @@
package com.example.mir4updater.activity;
package com.example.mir4updater.activity.update;
import com.example.mir4updater.R;
import com.example.mir4updater.base.mvvm.BaseMvvmActivity;
import com.example.mir4updater.databinding.ActivityMainBinding;
import com.example.mir4updater.databinding.ActivityUpdateBinding;
public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBinding> {
public class UpdateActivity extends BaseMvvmActivity<UpdateViewModel, ActivityUpdateBinding> {
@Override
public boolean setNightMode() {
return true;
}
@Override
public boolean setfitWindow() {
return true;
}
@Override
protected int getLayoutId() {
return R.layout.activity_main;
return R.layout.activity_update;
}
@Override
@@ -29,7 +39,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
}
public class BtnClick{
public class BtnClick {
}
}

View File

@@ -0,0 +1,18 @@
package com.example.mir4updater.activity.update;
import com.example.mir4updater.base.mvvm.BaseViewModel;
import com.example.mir4updater.databinding.ActivityUpdateBinding;
import com.trello.rxlifecycle4.android.ActivityEvent;
public class UpdateViewModel extends BaseViewModel <ActivityUpdateBinding, ActivityEvent>{
@Override
public ActivityUpdateBinding getVDBinding() {
return binding;
}
@Override
public void onDestroy() {
}
}

View File

@@ -0,0 +1,242 @@
package com.example.mir4updater.activity.upload;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import com.blankj.utilcode.util.GsonUtils;
import com.example.mir4updater.R;
import com.example.mir4updater.base.mvvm.BaseMvvmActivity;
import com.example.mir4updater.bean.FileItem;
import com.example.mir4updater.bean.UploadFilesInfo;
import com.example.mir4updater.config.CommonConfig;
import com.example.mir4updater.databinding.ActivityUploadBinding;
import com.example.mir4updater.utils.ApkUtils;
import com.example.mir4updater.utils.HashUtils;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class UploadActivity extends BaseMvvmActivity<UploadViewModel, ActivityUploadBinding> {
private static final String TAG = "UploadActivity";
private static final int ANDROID_REQUEST_CODE = 9819;
private DocumentFile mDocumentFile;
@Override
public boolean setNightMode() {
return true;
}
@Override
public boolean setfitWindow() {
return true;
}
@Override
protected int getLayoutId() {
return R.layout.activity_upload;
}
@Override
protected void initDataBinding() {
mViewModel.setCtx(this);
mViewModel.setLifecycle(getLifecycleSubject());
mViewModel.setVDBinding(mViewDataBinding);
mViewDataBinding.setClick(new BtnClick());
}
@Override
protected void initView() {
}
@Override
protected void initData() {
Log.e(TAG, "initData: " + getFilesDir());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.e(TAG, "onActivityResult: " + requestCode);
if (requestCode == ANDROID_REQUEST_CODE) {
startTraversal(mDocumentFile);
}
}
private void getPermssions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 请求Android/data目录的权限Android/obb目录则把data替换成obb即可。
Uri treeUri = Uri.parse(CommonConfig.MIR4_CONTENT_PATH);
mDocumentFile = DocumentFile.fromTreeUri(this, treeUri);
if (mDocumentFile != null) {
if (mDocumentFile.canRead()) {
startTraversal(mDocumentFile);
} else {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentFile.getUri());
startActivityForResult(intent, ANDROID_REQUEST_CODE);
}
} else {
}
}
// Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// startActivityForResult(intent, REQUEST_CODE);
}
// 在Activity中启动遍历
private List<FileItem> fileList = new ArrayList<>();
private void startTraversal(DocumentFile root) {
fileList.clear();
// 遍历完成后更新UI例如RecyclerView
traverseDocumentFile(root);
}
// 递归遍历DocumentFile
// 递归遍历并填充列表
private void traverseDocumentFile(DocumentFile documentFile, List<FileItem> resultList, String path) {
if (documentFile == null || !documentFile.exists()) return;
DocumentFile[] files = documentFile.listFiles();
if (files == null) return;
for (DocumentFile file : files) {
if (!file.isDirectory()) {
Log.i(TAG, "traverseDocumentFile: " + getFileMd5(file.getUri()));
}
// 创建条目并加入列表
FileItem item = new FileItem(
file.getName(),
file.isDirectory(),
file.length(),
file.getUri(),
path
);
resultList.add(item);
// 如果是目录则递归遍历
if (file.isDirectory()) {
traverseDocumentFile(file, resultList, path + file.getName() + "/");
}
}
}
/**
* 将会生成一个文件列表的json文件以文件的sha1作为唯一标识上传至服务器
*
* @param root
*/
private void traverseDocumentFile(DocumentFile root) {
Observable.create(new ObservableOnSubscribe<List<FileItem>>() {
@Override
public void subscribe(@NonNull ObservableEmitter<List<FileItem>> emitter) throws Throwable {
fileList.clear();
traverseDocumentFile(root, fileList, "/");
emitter.onNext(fileList);
}
}).compose(RxLifecycle.bindUntilEvent(getLifecycleSubject(), ActivityEvent.DESTROY))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<FileItem>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("traverseDocumentFile", "onSubscribe: ");
}
@Override
public void onNext(@NonNull List<FileItem> fileItems) {
Log.e("traverseDocumentFile", "onNext: " + fileItems);
Log.e("traverseDocumentFile", "onNext: " + fileItems.hashCode());
File dir = getExternalFilesDir(null); // 默认目录,可指定子目录如 "Documents"
File file = new File(dir, System.currentTimeMillis() + ".json");
try (FileWriter writer = new FileWriter(file)) {
UploadFilesInfo uploadFilesInfo = new UploadFilesInfo();
uploadFilesInfo.setHashCode(fileItems.hashCode());
uploadFilesInfo.setFiles(fileItems);
String content = GsonUtils.toJson(uploadFilesInfo);
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e("traverseDocumentFile", "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e("traverseDocumentFile", "onComplete: ");
mViewDataBinding.tvUpdate.setEnabled(true);
mViewDataBinding.tvUpdate.setText("创建更新");
}
});
}
public String getFileMd5(Uri uri) {
InputStream inputStream = null;
try {
inputStream = getContentResolver().openInputStream(uri);
if (inputStream != null) {
return HashUtils.calculateMd5(inputStream);
}
} catch (IOException | NoSuchAlgorithmException e) {
Log.e("FileHashUtils", "Error calculating MD5", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e("FileHashUtils", "Error closing stream", e);
}
}
}
return null;
}
public class BtnClick {
public void openMir4(View view) {
ApkUtils.openPackage(UploadActivity.this, CommonConfig.MIR4_PACKAGE_NAME);
}
public void createUpdate(View view) {
mViewDataBinding.tvUpdate.setEnabled(false);
mViewDataBinding.tvUpdate.setText("请稍候");
getPermssions();
}
}
}

View File

@@ -0,0 +1,18 @@
package com.example.mir4updater.activity.upload;
import com.example.mir4updater.base.mvvm.BaseViewModel;
import com.example.mir4updater.databinding.ActivityUploadBinding;
import com.trello.rxlifecycle4.android.ActivityEvent;
public class UploadViewModel extends BaseViewModel<ActivityUploadBinding, ActivityEvent> {
@Override
public ActivityUploadBinding getVDBinding() {
return binding;
}
@Override
public void onDestroy() {
}
}

View File

@@ -0,0 +1,56 @@
package com.example.mir4updater.bean;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.Serializable;
public class FileItem implements Serializable {
private static final long serialVersionUID = -2104809069386725078L;
private String name;
private boolean isDirectory;
private long size;
private Uri uri;
private String pathName;
public FileItem(String name, boolean isDirectory, long size, Uri uri,String path) {
this.name = name != null ? name : "Unnamed";
this.isDirectory = isDirectory;
this.size = isDirectory ? 0 : size; // 目录大小为0
this.uri = uri;
this.pathName = path;
}
// Getter方法
public String getName() {
return name;
}
public boolean isDirectory() {
return isDirectory;
}
public long getSize() {
return size;
}
public Uri getUri() {
return uri;
}
public String getPathName() {
return pathName;
}
@NonNull
@Override
public String toString() {
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
}
}

View File

@@ -0,0 +1,38 @@
package com.example.mir4updater.bean;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.Serializable;
import java.util.List;
public class UploadFilesInfo implements Serializable {
private static final long serialVersionUID = 6178596317886210704L;
long hashCode;
List<FileItem> files;
public long getHashCode() {
return hashCode;
}
public void setHashCode(long hashCode) {
this.hashCode = hashCode;
}
public List<FileItem> getFiles() {
return files;
}
public void setFiles( List<FileItem> files) {
this.files = files;
}
@NonNull
@Override
public String toString() {
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
}
}

View File

@@ -0,0 +1,8 @@
package com.example.mir4updater.config;
public class CommonConfig {
public static final String MMKV_ID = "InterProcessKV";
public static final String MIR4_PACKAGE_NAME ="com.wemade.mir4global";
public static final String MIR4_CONTENT_PATH = "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2Fcom.wemade.mir4global%2Ffiles";
}

View File

@@ -0,0 +1,90 @@
package com.example.mir4updater.utils;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;
import android.util.Log;
import com.hjq.toast.Toaster;
import java.util.List;
public class ApkUtils {
private static final String TAG = "ApkUtils";
public static boolean openPackage(Context context, String packageName) {
Context pkgContext = getPackageContext(context, packageName);
Intent intent = getAppOpenIntentByPackageName(context, packageName);
if (pkgContext != null && intent != null) {
pkgContext.startActivity(intent);
return true;
}
return false;
}
public static Context getPackageContext(Context context, String packageName) {
Context pkgContext = null;
if (context.getPackageName().equals(packageName)) {
pkgContext = context;
} else {
// 创建第三方应用的上下文环境
try {
pkgContext = context.createPackageContext(packageName,
Context.CONTEXT_IGNORE_SECURITY
| Context.CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
return pkgContext;
}
public static Intent getAppOpenIntentByPackageName(Context context, String packageName) {
//Activity完整名
String mainAct = null;
//根据包名寻找
PackageManager pkgMag = context.getPackageManager();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_NEW_TASK);
List<ResolveInfo> list = pkgMag.queryIntentActivities(intent, PackageManager.GET_ACTIVITIES);
for (int i = 0; i < list.size(); i++) {
ResolveInfo info = list.get(i);
if (info.activityInfo.packageName.equals(packageName)) {
mainAct = info.activityInfo.name;
break;
}
}
if (TextUtils.isEmpty(mainAct)) {
return null;
}
intent.setComponent(new ComponentName(packageName, mainAct));
return intent;
}
public static boolean openPackage(Context context, String packageName, String className) {
if (TextUtils.isEmpty(className)) {
return openPackage(context, packageName);
}
ComponentName cn = new ComponentName(packageName, className);
Intent intent = new Intent();
intent.setComponent(cn);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (context != null) {
try {
context.startActivity(intent);
return true;
} catch (Exception e) {
Log.e(TAG, "openPackage: " + e.getMessage());
Toaster.show("打开失败");
return false;
}
}
return false;
}
}

View File

@@ -0,0 +1,83 @@
package com.example.mir4updater.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashUtils {
public static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(file);
byte[] dataBytes = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(dataBytes)) != -1) {
md.update(dataBytes, 0, bytesRead);
}
byte[] mdBytes = md.digest();
StringBuilder sb = new StringBuilder();
for (byte mdByte : mdBytes) {
sb.append(Integer.toString((mdByte & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
public static String calculateMd5(InputStream inputStream)
throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
return bytesToHex(digest.digest());
}
public static String calculateSHA1(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes);
}
public static String calculateSHA256(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = String.format("%02x", b & 0xFF);
/*大写*/
// String hex = String.format("%02X", b & 0xFF);
hexString.append(hex);
}
return hexString.toString();
}
}

View File

@@ -0,0 +1,30 @@
package com.example.mir4updater.utils;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import androidx.documentfile.provider.DocumentFile;
import com.example.mir4updater.config.CommonConfig;
import com.hjq.permissions.Permission;
public class PermissionUtils {
public static final String[] Location_Permission = new String[]{
// Permission.READ_EXTERNAL_STORAGE,
// Permission.WRITE_EXTERNAL_STORAGE,
Permission.MANAGE_EXTERNAL_STORAGE,
};
public static boolean checkAppData(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 请求Android/data目录的权限Android/obb目录则把data替换成obb即可。
Uri treeUri = Uri.parse(CommonConfig.MIR4_CONTENT_PATH);
DocumentFile documentFile = DocumentFile.fromTreeUri(context, treeUri);
if (documentFile != null) {
return documentFile.canRead();
}
}
return false;
}
}

View File

@@ -0,0 +1,20 @@
package com.example.mir4updater.utils;
import android.content.Context;
import android.net.Uri;
import androidx.core.content.FileProvider;
import java.io.File;
public class UriUtils {
// 转换文件路径为 content:// Uri
public static Uri getUriFromFile(Context context, String filePath) {
File file = new File(filePath);
return FileProvider.getUriForFile(
context,
context.getPackageName() + ".fileprovider", // 必须与 Manifest 中的 authorities 一致
file
);
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 内部颜色 -->
<solid android:color="@color/lightgrey" />
<padding
android:bottom="8dp"
android:left="32dp"
android:right="32dp"
android:top="8dp" />
<!-- 圆角的幅度 -->
<corners
android:bottomLeftRadius="32dp"
android:bottomRightRadius="32dp"
android:topLeftRadius="32dp"
android:topRightRadius="32dp" />
</shape>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 内部颜色 -->
<solid android:color="@color/lightgreen" />
<padding
android:bottom="8dp"
android:left="32dp"
android:right="32dp"
android:top="8dp" />
<!-- 圆角的幅度 -->
<corners
android:bottomLeftRadius="32dp"
android:bottomRightRadius="32dp"
android:topLeftRadius="32dp"
android:topRightRadius="32dp" />
</shape>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 内部颜色 -->
<solid android:color="@color/blue" />
<padding
android:bottom="8dp"
android:left="32dp"
android:right="32dp"
android:top="8dp" />
<!-- 圆角的幅度 -->
<corners
android:bottomLeftRadius="32dp"
android:bottomRightRadius="32dp"
android:topLeftRadius="32dp"
android:topRightRadius="32dp" />
</shape>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 填充的颜色:这里设置背景透明 -->
<solid android:color="@color/white" />
<!-- 边框的颜色 :不能和窗口背景色一样 -->
<!-- 设置按钮的四个角为弧形 -->
<!-- android:radius 弧形的半径 -->
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
<!-- paddingButton里面的文字与Button边界的间隔 -->
<!-- <padding-->
<!-- android:bottom="10dp"-->
<!-- android:left="10dp"-->
<!-- android:right="10dp"-->
<!-- android:top="10dp" />-->
</shape>

View File

@@ -2,26 +2,140 @@
<layout 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"
tools:context=".activity.MainActivity">
tools:context=".activity.main.MainActivity">
<data>
<variable
name="click"
type="com.example.mir4updater.activity.MainActivity.BtnClick" />
type="com.example.mir4updater.activity.main.MainActivity.BtnClick" />
<variable
name="storagePermission"
type="Boolean" />
<variable
name="appStoragePermission"
type="Boolean" />
<import type="android.view.View" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_upload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bt_upload_bg"
android:maxLines="1"
android:onClick="@{click::upload}"
android:singleLine="true"
android:text="上传"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@drawable/bt_update_bg"
android:maxLines="1"
android:onClick="@{click::update}"
android:singleLine="true"
android:text="更新"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_upload" />
<TextView
android:id="@+id/tv_storage_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:maxLines="1"
android:padding="8dp"
android:singleLine="true"
android:text="@{`存储权限状态:`+(storagePermission==true?`已获取`:`未获取`)}"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_update"
tools:text="存储权限状态" />
<TextView
android:id="@+id/tv_storage_permission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/bt_permission_bg"
android:gravity="center"
android:maxLines="1"
android:singleLine="true"
android:text="点击获取存储权限"
android:onClick="@{click::getStoragePermission}"
android:textColor="@color/white"
android:textSize="18sp"
android:visibility="@{storagePermission==true?View.GONE:View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_storage_state" />
<TextView
android:id="@+id/tv_app_storage_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:maxLines="1"
android:padding="8dp"
android:singleLine="true"
android:text="@{`APP内部存储权限状态:`+(appStoragePermission==true?`已获取`:`未获取`)}"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_storage_permission"
tools:text="APP内部存储权限状态" />
<TextView
android:id="@+id/tv_app_storage_permission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/bt_permission_bg"
android:gravity="center"
android:maxLines="1"
android:singleLine="true"
android:text="点击获取APP内部存储权限"
android:onClick="@{click::getAppStoragePermission}"
android:textColor="@color/white"
android:textSize="18sp"
android:visibility="@{appStoragePermission==true?View.GONE:View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_app_storage_state" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context=".activity.update.UpdateActivity">
<data>
<variable
name="click"
type="com.example.mir4updater.activity.update.UpdateActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context=".activity.upload.UploadActivity">
<data>
<variable
name="click"
type="com.example.mir4updater.activity.upload.UploadActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/upload_note"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bt_update_bg"
android:maxLines="1"
android:onClick="@{click::openMir4}"
android:singleLine="true"
android:text="打开MIR4"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<TextView
android:id="@+id/tv_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bt_upload_bg"
android:maxLines="1"
android:onClick="@{click::createUpdate}"
android:singleLine="true"
android:text="创建更新"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_open" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="@drawable/dialog_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="权限申请说明"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toTopOf="@+id/linearLayout5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/black"
android:textSize="18sp"
tools:text="内容文本" />
</ScrollView>
<LinearLayout
android:id="@+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="拒绝"
android:textColor="@color/red"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_content" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="同意"
android:textColor="@color/blue"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/tv_content"
app:layout_constraintTop_toBottomOf="@+id/tv_content" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -3,4 +3,245 @@
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<!--https://www.jianshu.com/p/8dc258dfd189-->
<color name="transparent">#00000000</color><!--透明色-->
<color name="colorYang">#FFE2C59B</color><!--羊皮纸色-->
<color name="ivory">#FFFFFFF0</color><!--象牙色-->
<color name="lightYellow">#FFFFFFE0</color><!--亮黄色-->
<color name="yellow">#FFFFFF00</color><!--黄色-->
<color name="snow">#FFFFFAFA</color><!--雪白色-->
<color name="floralWhite">#FFFFFAF0</color><!--花白色-->
<color name="lemonChiffon">#FFFFFACD</color><!--柠檬绸色-->
<color name="cornSilk">#FFFFF8DC</color><!--米绸色-->
<color name="seashell">#FFFFF5EE</color><!--海贝色-->
<color name="lavenderBlush">#FFFFF0F5</color><!--淡紫红色-->
<color name="papayaWhip">#FFFFEFD5</color><!--番木色-->
<color name="blanchedAlmond">#FFFFEBCD</color><!--白杏色-->
<color name="mistyRose">#FFFFE4E1</color><!--浅玫瑰色-->
<color name="bisque">#FFFFE4C4</color><!--桔黄色-->
<color name="moccasin">#FFFFE4B5</color><!--鹿皮色-->
<color name="navajoWhite">#FFFFDEAD</color><!--纳瓦白-->
<color name="peachPuff">#FFFFDAB9</color><!--桃色-->
<color name="gold">#FFFFD700</color><!--金色-->
<color name="pink">#FFFFC0CB</color><!--粉红色-->
<color name="lightPink">#FFFFB6C1</color><!--亮粉红色-->
<color name="orange">#FFFFA500</color><!--橙色-->
<color name="lightSalmon">#FFFFA07A</color><!--亮肉色-->
<color name="darkOrange">#FFFF8C00</color><!--暗桔黄色-->
<color name="coral">#FFFF7F50</color><!--珊瑚色-->
<color name="hotPink">#FFFF69B4</color><!--热粉红色-->
<color name="tomato">#FFFF6347</color><!--西红柿色-->
<color name="orangeRed">#FFFF4500</color><!--红橙色-->
<color name="deepPink">#FFFF1493</color><!--深粉红色-->
<color name="fuchsia">#FFFF00FF</color><!--紫红色-->
<color name="red">#FFFF0000</color><!--红色-->
<color name="oldLace">#FFFDF5E6</color><!--老花色-->
<color name="lightGoldenrodYellow">#FFFAFAD2</color><!--亮金黄色-->
<color name="linen">#FFFAF0E6</color><!--亚麻色-->
<color name="antiqueWhite">#FFFAEBD7</color><!--古董白色-->
<color name="salmon">#FFFA8072</color><!--鲜肉色-->
<color name="ghostWhite">#FFF8F8FF</color><!--幽灵白-->
<color name="mintCream">#FFF5FFFA</color><!--薄荷色-->
<color name="whiteSmoke">#FFF5F5F5</color><!--烟白色-->
<color name="beige">#FFF5F5DC</color><!--米色-->
<color name="wheat">#FFF5DEB3</color><!--浅黄色-->
<color name="sandyBrown">#FFF4A460</color><!--沙褐色-->
<color name="azure">#FFF0FFFF</color><!--天蓝色-->
<color name="aliceBlue">#FFF0F8FF</color><!--艾利斯兰色-->
<color name="khaki">#FFF0E68C</color><!--黄褐色-->
<color name="lightCoral">#FFF08080</color><!--亮珊瑚色-->
<color name="paleGoldenrod">#FFEEE8AA</color><!--苍麒麟色-->
<color name="violet">#FFEE82EE</color><!--紫罗兰色-->
<color name="darkSalmon">#FFE9967A</color><!--暗肉色-->
<color name="lavender">#FFE6E6FA</color><!--淡紫色-->
<color name="lightCyan">#FFE0FFFF</color><!--亮青色-->
<color name="burlyWood">#FFDEB887</color><!--实木色-->
<color name="plum">#FFDDA0DD</color><!--洋李色-->
<color name="lightGrey">#FFDCDCDC</color><!--淡灰色-->
<color name="crimson">#FFDC143C</color><!--暗深红色-->
<color name="paleVioletRed">#FFDB7093</color><!--苍紫罗兰色-->
<color name="goldenrod">#FFDAA520</color><!--金麒麟色-->
<color name="orchid">#FFDA70D6</color><!--淡紫色-->
<color name="thistle">#FFD8BFD8</color><!--蓟色-->
<color name="lightGray">#FFD3D3D3</color><!--亮灰色-->
<color name="tan">#FFD2B48C</color><!--茶色-->
<color name="chocolate">#FFD2691E</color><!--巧可力色-->
<color name="peru">#FFCD853F</color><!--秘鲁色-->
<color name="indianRed">#FFCD5C5C</color><!--印第安红色-->
<color name="mediumVioletRed">#FFC71585</color><!--中紫罗兰色-->
<color name="silver">#FFC0C0C0</color><!--银色-->
<color name="darkKhaki">#FFBDB76B</color><!--暗黄褐色-->
<color name="rosyBrown">#FFBC8F8F</color><!--褐玫瑰红色-->
<color name="mediumOrchid">#FFBA55D3</color><!--中粉紫色-->
<color name="darkGoldenrod">#FFB8860B</color><!--暗金黄色-->
<color name="firebrick">#FFB22222</color><!--火砖色-->
<color name="powderBlue">#FFB0E0E6</color><!--粉蓝色-->
<color name="lightSteelBlue">#FFB0C4DE</color><!--亮钢兰色-->
<color name="paleTurquoise">#FFAFEEEE</color><!--苍宝石绿色-->
<color name="greenYellow">#FFADFF2F</color><!--黄绿色-->
<color name="lightBlue">#FFADD8E6</color><!--亮蓝色-->
<color name="darkGray">#FFA9A9A9</color><!--暗灰色-->
<color name="brown">#FFA52A2A</color><!--褐色-->
<color name="sienna">#FFA0522D</color><!--赭色-->
<color name="darkOrchid">#FF9932CC</color><!--暗紫色-->
<color name="paleGreen">#FF98FB98</color><!--苍绿色-->
<color name="darkViolet">#FF9400D3</color><!--暗紫罗兰色-->
<color name="mediumPurple">#FF9370DB</color><!--中紫色-->
<color name="lightGreen">#FF90EE90</color><!--亮绿色-->
<color name="darkSeaGreen">#FF8FBC8F</color><!--暗海兰色-->
<color name="saddleBrown">#FF8B4513</color><!--重褐色-->
<color name="darkMagenta">#FF8B008B</color><!--暗洋红色-->
<color name="darkRed">#FF8B0000</color><!--暗红色-->
<color name="blueViolet">#FF8A2BE2</color><!--紫罗兰蓝色-->
<color name="lightSkyBlue">#FF87CEFA</color><!--亮天蓝色-->
<color name="skyBlue">#FF87CEEB</color><!--天蓝色-->
<color name="gray">#FF808080</color><!--灰色-->
<color name="olive">#FF808000</color><!--橄榄色-->
<color name="purple">#FF800080</color><!--紫色-->
<color name="maroon">#FF800000</color><!--粟色-->
<color name="aquamarine">#FF7FFFD4</color><!--碧绿色-->
<color name="chartreuse">#FF7FFF00</color><!--黄绿色-->
<color name="lawnGreen">#FF7CFC00</color><!--草绿色-->
<color name="mediumSlateBlue">#FF7B68EE</color><!--中暗蓝色-->
<color name="lightSlateGray">#FF778899</color><!--亮蓝灰色-->
<color name="slateGray">#FF708090</color><!--灰石色-->
<color name="oliveDrab">#FF6B8E23</color><!--深绿褐色-->
<color name="slateBlue">#FF6A5ACD</color><!--石蓝色-->
<color name="dimGray">#FF696969</color><!--暗灰色-->
<color name="mediumAquamarine">#FF66CDAA</color><!--中绿色-->
<color name="cornFlowerBlue">#FF6495ED</color><!--菊兰色-->
<color name="cadetBlue">#FF5F9EA0</color><!--军兰色-->
<color name="darkOliveGreen">#FF556B2F</color><!--暗橄榄绿色-->
<color name="indigo">#FF4B0082</color><!--靛青色-->
<color name="mediumTurquoise">#FF48D1CC</color><!--中绿宝石色-->
<color name="darkSlateBlue">#FF483D8B</color><!--暗灰蓝色-->
<color name="steelBlue">#FF4682B4</color><!--钢兰色-->
<color name="royalBlue">#FF4169E1</color><!--皇家蓝色-->
<color name="turquoise">#FF40E0D0</color><!--青绿色-->
<color name="mediumSeaGreen">#FF3CB371</color><!--中海蓝色-->
<color name="limeGreen">#FF32CD32</color><!--橙绿色-->
<color name="darkSlateGray">#FF2F4F4F</color><!--暗瓦灰色-->
<color name="seaGreen">#FF2E8B57</color><!--海绿色-->
<color name="forestGreen">#FF228B22</color><!--森林绿色-->
<color name="lightSeaGreen">#FF20B2AA</color><!--亮海蓝色-->
<color name="dodgerBlue">#FF1E90FF</color><!--闪兰色-->
<color name="midnightBlue">#FF191970</color><!--中灰兰色-->
<color name="aqua">#FF00FFFF</color><!--浅绿色-->
<color name="springGreen">#FF00FF7F</color><!--春绿色-->
<color name="lime">#FF00FF00</color><!--酸橙色-->
<color name="mediumSpringGreen">#FF00FA9A</color><!--中春绿色-->
<color name="darkTurquoise">#FF00CED1</color><!--暗宝石绿色-->
<color name="deepSkyBlue">#FF00BFFF</color><!--深天蓝色-->
<color name="darkCyan">#FF008B8B</color><!--暗青色-->
<color name="teal">#FF008080</color><!--水鸭色-->
<color name="green">#FF008000</color><!--绿色-->
<color name="darkGreen">#FF006400</color><!--暗绿色-->
<color name="blue">#FF0000FF</color><!--蓝色-->
<color name="mediumBlue">#FF0000CD</color><!--中兰色-->
<color name="darkBlue">#FF00008B</color><!--暗蓝色-->
<color name="navy">#FF000080</color><!--海军色-->
<color name="unBlack">#FF2B2B2B</color><!--不知名黑色-->
<!--https://www.cnblogs.com/elleniou/archive/2012/04/25/2469676.html-->
<color name="white">#ffffff</color><!--白色 -->
<color name="lightyellow">#ffffe0</color><!--亮黄色 -->
<color name="floralwhite">#fffaf0</color><!--花白色 -->
<color name="lemonchiffon">#fffacd</color><!--柠檬绸色 -->
<color name="cornsilk">#fff8dc</color><!--米绸色 -->
<color name="seaShell">#fff5ee</color><!--海贝色 -->
<color name="lavenderblush">#fff0f5</color><!--淡紫红 -->
<color name="papayawhip">#ffefd5</color><!--番木色 -->
<color name="blanchedalmond">#ffebcd</color><!--白杏色 -->
<color name="mistyrose">#ffe4e1</color><!--浅玫瑰色 -->
<color name="navajowhite">#ffdead</color><!--纳瓦白 -->
<color name="peachpuff">#ffdab9</color><!--桃色 -->
<color name="lightpink">#ffb6c1</color><!--亮粉红色 -->
<color name="lightsalmon">#ffa07a</color><!--亮肉色 -->
<color name="darkorange">#ff8c00</color><!--暗桔黄色 -->
<color name="hotpink">#ff69b4</color><!--热粉红色 -->
<color name="orangered">#ff4500</color><!--红橙色 -->
<color name="deeppink">#ff1493</color><!--深粉红色 -->
<color name="magenta">#ff00ff</color><!--红紫色 -->
<color name="oldlace">#fdf5e6</color><!--老花色 -->
<color name="lightgoldenrodyellow">#fafad2</color><!--亮金黄色 -->
<color name="antiquewhite">#faebd7</color><!--古董白 -->
<color name="ghostwhite">#f8f8ff</color><!--幽灵白 -->
<color name="mintcream">#f5fffa</color><!--薄荷色 -->
<color name="whitesmoke">#f5f5f5</color><!--烟白色 -->
<color name="sandybrown">#f4a460</color><!--沙褐色 -->
<color name="honeydew">#f0fff0</color><!--蜜色 -->
<color name="aliceblue">#f0f8ff</color><!--艾利斯兰 -->
<color name="lightcoral">#f08080</color><!--亮珊瑚色 -->
<color name="palegoldenrod">#eee8aa</color><!--苍麒麟色 -->
<color name="darksalmon">#e9967a</color><!--暗肉色 -->
<color name="lightcyan">#e0ffff</color><!--亮青色 -->
<color name="burlywood">#deb887</color><!--实木色 -->
<color name="gainsboro">#dcdcdc</color><!--淡灰色 -->
<color name="palevioletred">#db7093</color><!--苍紫罗兰色 -->
<color name="lightgray">#d3d3d3</color><!--亮灰色 -->
<color name="lightgrey">#d3d3d3</color><!--亮灰色 -->
<color name="indianred">#cd5c5c</color><!--印第安红 -->
<color name="mediumvioletred">#c71585</color><!--中紫罗兰色 -->
<color name="darkkhaki">#bdb76b</color><!--暗黄褐色 -->
<color name="rosybrown">#bc8f8f</color><!--褐玫瑰红 -->
<color name="mediumorchid">#ba55d3</color><!--中粉紫色 -->
<color name="darkgoldenrod">#b8860b</color><!--暗金黄色 -->
<color name="powderblue">#b0e0e6</color><!--粉蓝色 -->
<color name="lightsteelblue">#b0c4de</color><!--亮钢兰色 -->
<color name="paleturquoise">#afeeee</color><!--苍宝石绿 -->
<color name="greenyellow">#adff2f</color><!--黄绿色 -->
<color name="lightblue">#add8e6</color><!--亮蓝色 -->
<color name="darkgray">#a9a9a9</color><!--暗灰色 -->
<color name="darkgrey">#a9a9a9</color><!--暗灰色 -->
<color name="darkorchid">#9932cc</color><!--暗紫色 -->
<color name="palegreen">#98fb98</color><!--苍绿色 -->
<color name="darkviolet">#9400d3</color><!--暗紫罗兰色 -->
<color name="mediumpurple">#9370db</color><!--中紫色 -->
<color name="lightgreen">#90ee90</color><!--亮绿色 -->
<color name="darkseagreen">#8fbc8f</color><!--暗海兰色 -->
<color name="saddlebrown">#8b4513</color><!--重褐色 -->
<color name="darkmagenta">#8b008b</color><!--暗洋红 -->
<color name="darkred">#8b0000</color><!--暗红色 -->
<color name="blueviolet">#8a2be2</color><!--紫罗兰蓝色 -->
<color name="lightskyblue">#87cefa</color><!--亮天蓝色 -->
<color name="skyblue">#87ceeb</color><!--天蓝色 -->
<color name="grey">#808080</color><!--灰色 -->
<color name="lawngreen">#7cfc00</color><!--草绿色 -->
<color name="mediumslateblue">#7b68ee</color><!--中暗蓝色 -->
<color name="lightslategray">#778899</color><!--亮蓝灰 -->
<color name="lightslategrey">#778899</color><!--亮蓝灰 -->
<color name="slategray">#708090</color><!--灰石色 -->
<color name="slategrey">#708090</color><!--灰石色 -->
<color name="olivedrab">#6b8e23</color><!--深绿褐色 -->
<color name="slateblue">#6a5acd</color><!--石蓝色 -->
<color name="dimgray">#696969</color><!--暗灰色 -->
<color name="dimgrey">#696969</color><!--暗灰色 -->
<color name="mediumaquamarine">#66cdaa</color><!--中绿色 -->
<color name="cornflowerblue">#6495ed</color><!--菊兰色 -->
<color name="cadetblue">#5f9ea0</color><!--军兰色 -->
<color name="darkolivegreen">#556b2f</color><!--暗橄榄绿 -->
<color name="mediumturquoise">#48d1cc</color><!--中绿宝石 -->
<color name="darkslateblue">#483d8b</color><!--暗灰蓝色 -->
<color name="steelblue">#4682b4</color><!--钢兰色 -->
<color name="royalblue">#4169e1</color><!--皇家蓝 -->
<color name="mediumseagreen">#3cb371</color><!--中海蓝 -->
<color name="limegreen">#32cd32</color><!--橙绿色 -->
<color name="darkslategray">#2f4f4f</color><!--暗瓦灰色 -->
<color name="darkslategrey">#2f4f4f</color><!--暗瓦灰色 -->
<color name="seagreen">#2e8b57</color><!--海绿色 -->
<color name="forestgreen">#228b22</color><!--森林绿 -->
<color name="lightseagreen">#20b2aa</color><!--亮海蓝色 -->
<color name="dodgerblue">#1e90ff</color><!--闪兰色 -->
<color name="midnightblue">#191970</color><!--中灰兰色 -->
<color name="cyan">#00ffff</color><!--青色 -->
<color name="springgreen">#00ff7f</color><!--春绿色 -->
<color name="mediumspringgreen">#00fa9a</color><!--中春绿色 -->
<color name="darkturquoise">#00ced1</color><!--暗宝石绿 -->
<color name="deepskyblue">#00bfff</color><!--深天蓝色 -->
<color name="darkcyan">#008b8b</color><!--暗青色 -->
<color name="darkgreen">#006400</color><!--暗绿色 -->
<color name="mediumblue">#0000cd</color><!--中兰色 -->
<color name="darkblue">#00008b</color><!--暗蓝色 -->
<color name="black">#000000</color><!--黑色 -->
</resources>

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name">Mir4Updater</string>
<string name="app_name">MIR4Updater</string>
<string name="upload_note">请先确保MIR4已经更新到最新版本\n点击 创建更新 会计算app内部所有文件的sha1\n点击 上传 将会把MIR4内部的所有pak文件上传至服务器\n计算和上传用时可能会很久</string>
</resources>

View File

@@ -1,7 +1,14 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">-->
<!-- &lt;!&ndash; Customize your theme here. &ndash;&gt;-->
<!-- <item name="colorPrimary">@color/colorPrimary</item>-->
<!-- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>-->
<!-- <item name="colorAccent">@color/colorAccent</item>-->
<!-- </style>-->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>