diff --git a/app/build.gradle b/app/build.gradle
index 7061634..2b5efb7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,13 +9,13 @@ def releaseTime() {
}
android {
- compileSdkVersion 29
+ compileSdkVersion 30
// buildToolsVersion "34.0.0"
defaultConfig {
applicationId "com.example.mir4updater"
minSdkVersion 24
- targetSdkVersion 29
+ targetSdkVersion 30
versionCode 1
versionName "1.0"
@@ -107,6 +107,7 @@ android {
}
}
}
+
}
@@ -120,12 +121,16 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.fragment:fragment:1.4.1"
+ implementation "androidx.documentfile:documentfile:1.0.1"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
+ implementation 'com.google.android.play:app-update:2.1.0'
+ implementation 'com.google.android.play:app-update-ktx:2.1.0' // Kotlin扩展
+
//RxJava
implementation 'io.reactivex.rxjava3:rxjava:3.0.0'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
@@ -144,7 +149,6 @@ dependencies {
//google
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.zxing:core:3.5.0'
- implementation 'com.google.android.material:material:1.4.0'
//生命周期管理
implementation 'com.trello.rxlifecycle4:rxlifecycle:4.0.2'
implementation 'com.trello.rxlifecycle4:rxlifecycle-android:4.0.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0c685c8..e527a45 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,22 +1,51 @@
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/mir4updater/activity/main/MainActivity.java b/app/src/main/java/com/example/mir4updater/activity/main/MainActivity.java
new file mode 100644
index 0000000..dcc616b
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/activity/main/MainActivity.java
@@ -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 {
+ 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 appUpdateInfoTask = mAppUpdateManager.getAppUpdateInfo();
+// appUpdateInfoTask.addOnSuccessListener(new OnSuccessListener() {
+// @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 fileList = new ArrayList<>();
+
+ private void startTraversal(DocumentFile root) {
+ fileList.clear();
+ // 遍历完成后更新UI(例如RecyclerView)
+ traverseDocumentFile(root);
+ }
+
+ // 递归遍历DocumentFile
+ // 递归遍历并填充列表
+ private void traverseDocumentFile(DocumentFile documentFile, List 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>() {
+ @Override
+ public void subscribe(@io.reactivex.rxjava3.annotations.NonNull ObservableEmitter> 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>() {
+ @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 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 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 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();
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/activity/MainViewModel.java b/app/src/main/java/com/example/mir4updater/activity/main/MainViewModel.java
similarity index 89%
rename from app/src/main/java/com/example/mir4updater/activity/MainViewModel.java
rename to app/src/main/java/com/example/mir4updater/activity/main/MainViewModel.java
index d05a4b1..d19c3e3 100644
--- a/app/src/main/java/com/example/mir4updater/activity/MainViewModel.java
+++ b/app/src/main/java/com/example/mir4updater/activity/main/MainViewModel.java
@@ -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;
diff --git a/app/src/main/java/com/example/mir4updater/activity/MainActivity.java b/app/src/main/java/com/example/mir4updater/activity/update/UpdateActivity.java
similarity index 54%
rename from app/src/main/java/com/example/mir4updater/activity/MainActivity.java
rename to app/src/main/java/com/example/mir4updater/activity/update/UpdateActivity.java
index d86999c..3c96f93 100644
--- a/app/src/main/java/com/example/mir4updater/activity/MainActivity.java
+++ b/app/src/main/java/com/example/mir4updater/activity/update/UpdateActivity.java
@@ -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 {
+public class UpdateActivity extends BaseMvvmActivity {
+
+ @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{
+
+ @Override
+ public ActivityUpdateBinding getVDBinding() {
+ return binding;
+ }
+
+ @Override
+ public void onDestroy() {
+
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/activity/upload/UploadActivity.java b/app/src/main/java/com/example/mir4updater/activity/upload/UploadActivity.java
new file mode 100644
index 0000000..20d20be
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/activity/upload/UploadActivity.java
@@ -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 {
+ 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 fileList = new ArrayList<>();
+
+ private void startTraversal(DocumentFile root) {
+ fileList.clear();
+ // 遍历完成后更新UI(例如RecyclerView)
+ traverseDocumentFile(root);
+ }
+
+ // 递归遍历DocumentFile
+ // 递归遍历并填充列表
+ private void traverseDocumentFile(DocumentFile documentFile, List 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>() {
+ @Override
+ public void subscribe(@NonNull ObservableEmitter> 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>() {
+ @Override
+ public void onSubscribe(@NonNull Disposable d) {
+ Log.e("traverseDocumentFile", "onSubscribe: ");
+ }
+
+ @Override
+ public void onNext(@NonNull List 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();
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/activity/upload/UploadViewModel.java b/app/src/main/java/com/example/mir4updater/activity/upload/UploadViewModel.java
new file mode 100644
index 0000000..2132e15
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/activity/upload/UploadViewModel.java
@@ -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 {
+
+ @Override
+ public ActivityUploadBinding getVDBinding() {
+ return binding;
+ }
+
+ @Override
+ public void onDestroy() {
+
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/bean/FileItem.java b/app/src/main/java/com/example/mir4updater/bean/FileItem.java
new file mode 100644
index 0000000..2d11c70
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/bean/FileItem.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/mir4updater/bean/UploadFilesInfo.java b/app/src/main/java/com/example/mir4updater/bean/UploadFilesInfo.java
new file mode 100644
index 0000000..c154efb
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/bean/UploadFilesInfo.java
@@ -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 files;
+
+ public long getHashCode() {
+ return hashCode;
+ }
+
+ public void setHashCode(long hashCode) {
+ this.hashCode = hashCode;
+ }
+
+ public List getFiles() {
+ return files;
+ }
+
+ public void setFiles( List files) {
+ this.files = files;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/config/CommonConfig.java b/app/src/main/java/com/example/mir4updater/config/CommonConfig.java
new file mode 100644
index 0000000..d5b5b07
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/config/CommonConfig.java
@@ -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";
+
+}
diff --git a/app/src/main/java/com/example/mir4updater/utils/ApkUtils.java b/app/src/main/java/com/example/mir4updater/utils/ApkUtils.java
new file mode 100644
index 0000000..85e4fd2
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/utils/ApkUtils.java
@@ -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 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;
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/utils/HashUtils.java b/app/src/main/java/com/example/mir4updater/utils/HashUtils.java
new file mode 100644
index 0000000..8b8548d
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/utils/HashUtils.java
@@ -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();
+ }
+}
+
diff --git a/app/src/main/java/com/example/mir4updater/utils/PermissionUtils.java b/app/src/main/java/com/example/mir4updater/utils/PermissionUtils.java
new file mode 100644
index 0000000..0af48c5
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/utils/PermissionUtils.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/com/example/mir4updater/utils/UriUtils.java b/app/src/main/java/com/example/mir4updater/utils/UriUtils.java
new file mode 100644
index 0000000..c57168e
--- /dev/null
+++ b/app/src/main/java/com/example/mir4updater/utils/UriUtils.java
@@ -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
+ );
+ }
+}
diff --git a/app/src/main/res/drawable/bt_permission_bg.xml b/app/src/main/res/drawable/bt_permission_bg.xml
new file mode 100644
index 0000000..211e31f
--- /dev/null
+++ b/app/src/main/res/drawable/bt_permission_bg.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bt_update_bg.xml b/app/src/main/res/drawable/bt_update_bg.xml
new file mode 100644
index 0000000..d6f2998
--- /dev/null
+++ b/app/src/main/res/drawable/bt_update_bg.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bt_upload_bg.xml b/app/src/main/res/drawable/bt_upload_bg.xml
new file mode 100644
index 0000000..21f3b5f
--- /dev/null
+++ b/app/src/main/res/drawable/bt_upload_bg.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml
new file mode 100644
index 0000000..70157ff
--- /dev/null
+++ b/app/src/main/res/drawable/dialog_background.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 9a83d3f..d7de177 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -2,26 +2,140 @@
+ tools:context=".activity.main.MainActivity">
+
+ type="com.example.mir4updater.activity.main.MainActivity.BtnClick" />
+
+
+
+
+
+
+
-
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_update.xml b/app/src/main/res/layout/activity_update.xml
new file mode 100644
index 0000000..80debbf
--- /dev/null
+++ b/app/src/main/res/layout/activity_update.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload.xml b/app/src/main/res/layout/activity_upload.xml
new file mode 100644
index 0000000..78bf8a4
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_permissions.xml b/app/src/main/res/layout/dialog_permissions.xml
new file mode 100644
index 0000000..cec6d24
--- /dev/null
+++ b/app/src/main/res/layout/dialog_permissions.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 030098f..6dfa484 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,245 @@
#6200EE
#3700B3
#03DAC5
+
+
+ #00000000
+ #FFE2C59B
+ #FFFFFFF0
+ #FFFFFFE0
+ #FFFFFF00
+ #FFFFFAFA
+ #FFFFFAF0
+ #FFFFFACD
+ #FFFFF8DC
+ #FFFFF5EE
+ #FFFFF0F5
+ #FFFFEFD5
+ #FFFFEBCD
+ #FFFFE4E1
+ #FFFFE4C4
+ #FFFFE4B5
+ #FFFFDEAD
+ #FFFFDAB9
+ #FFFFD700
+ #FFFFC0CB
+ #FFFFB6C1
+ #FFFFA500
+ #FFFFA07A
+ #FFFF8C00
+ #FFFF7F50
+ #FFFF69B4
+ #FFFF6347
+ #FFFF4500
+ #FFFF1493
+ #FFFF00FF
+ #FFFF0000
+ #FFFDF5E6
+ #FFFAFAD2
+ #FFFAF0E6
+ #FFFAEBD7
+ #FFFA8072
+ #FFF8F8FF
+ #FFF5FFFA
+ #FFF5F5F5
+ #FFF5F5DC
+ #FFF5DEB3
+ #FFF4A460
+ #FFF0FFFF
+ #FFF0F8FF
+ #FFF0E68C
+ #FFF08080
+ #FFEEE8AA
+ #FFEE82EE
+ #FFE9967A
+ #FFE6E6FA
+ #FFE0FFFF
+ #FFDEB887
+ #FFDDA0DD
+ #FFDCDCDC
+ #FFDC143C
+ #FFDB7093
+ #FFDAA520
+ #FFDA70D6
+ #FFD8BFD8
+ #FFD3D3D3
+ #FFD2B48C
+ #FFD2691E
+ #FFCD853F
+ #FFCD5C5C
+ #FFC71585
+ #FFC0C0C0
+ #FFBDB76B
+ #FFBC8F8F
+ #FFBA55D3
+ #FFB8860B
+ #FFB22222
+ #FFB0E0E6
+ #FFB0C4DE
+ #FFAFEEEE
+ #FFADFF2F
+ #FFADD8E6
+ #FFA9A9A9
+ #FFA52A2A
+ #FFA0522D
+ #FF9932CC
+ #FF98FB98
+ #FF9400D3
+ #FF9370DB
+ #FF90EE90
+ #FF8FBC8F
+ #FF8B4513
+ #FF8B008B
+ #FF8B0000
+ #FF8A2BE2
+ #FF87CEFA
+ #FF87CEEB
+ #FF808080
+ #FF808000
+ #FF800080
+ #FF800000
+ #FF7FFFD4
+ #FF7FFF00
+ #FF7CFC00
+ #FF7B68EE
+ #FF778899
+ #FF708090
+ #FF6B8E23
+ #FF6A5ACD
+ #FF696969
+ #FF66CDAA
+ #FF6495ED
+ #FF5F9EA0
+ #FF556B2F
+ #FF4B0082
+ #FF48D1CC
+ #FF483D8B
+ #FF4682B4
+ #FF4169E1
+ #FF40E0D0
+ #FF3CB371
+ #FF32CD32
+ #FF2F4F4F
+ #FF2E8B57
+ #FF228B22
+ #FF20B2AA
+ #FF1E90FF
+ #FF191970
+ #FF00FFFF
+ #FF00FF7F
+ #FF00FF00
+ #FF00FA9A
+ #FF00CED1
+ #FF00BFFF
+ #FF008B8B
+ #FF008080
+ #FF008000
+ #FF006400
+ #FF0000FF
+ #FF0000CD
+ #FF00008B
+ #FF000080
+ #FF2B2B2B
+
+
+ #ffffff
+ #ffffe0
+ #fffaf0
+ #fffacd
+ #fff8dc
+ #fff5ee
+ #fff0f5
+ #ffefd5
+ #ffebcd
+ #ffe4e1
+ #ffdead
+ #ffdab9
+ #ffb6c1
+ #ffa07a
+ #ff8c00
+ #ff69b4
+ #ff4500
+ #ff1493
+ #ff00ff
+ #fdf5e6
+ #fafad2
+ #faebd7
+ #f8f8ff
+ #f5fffa
+ #f5f5f5
+ #f4a460
+ #f0fff0
+ #f0f8ff
+ #f08080
+ #eee8aa
+ #e9967a
+ #e0ffff
+ #deb887
+ #dcdcdc
+ #db7093
+ #d3d3d3
+ #d3d3d3
+ #cd5c5c
+ #c71585
+ #bdb76b
+ #bc8f8f
+ #ba55d3
+ #b8860b
+ #b0e0e6
+ #b0c4de
+ #afeeee
+ #adff2f
+ #add8e6
+ #a9a9a9
+ #a9a9a9
+ #9932cc
+ #98fb98
+ #9400d3
+ #9370db
+ #90ee90
+ #8fbc8f
+ #8b4513
+ #8b008b
+ #8b0000
+ #8a2be2
+ #87cefa
+ #87ceeb
+ #808080
+ #7cfc00
+ #7b68ee
+ #778899
+ #778899
+ #708090
+ #708090
+ #6b8e23
+ #6a5acd
+ #696969
+ #696969
+ #66cdaa
+ #6495ed
+ #5f9ea0
+ #556b2f
+ #48d1cc
+ #483d8b
+ #4682b4
+ #4169e1
+ #3cb371
+ #32cd32
+ #2f4f4f
+ #2f4f4f
+ #2e8b57
+ #228b22
+ #20b2aa
+ #1e90ff
+ #191970
+ #00ffff
+ #00ff7f
+ #00fa9a
+ #00ced1
+ #00bfff
+ #008b8b
+ #006400
+ #0000cd
+ #00008b
+ #000000
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d8eaa91..9f2e5c6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
- Mir4Updater
+ MIR4Updater
+ 请先确保MIR4已经更新到最新版本\n点击 创建更新 会计算app内部所有文件的sha1\n点击 上传 将会把MIR4内部的所有pak文件上传至服务器\n计算和上传用时可能会很久
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 5885930..d9a3652 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,7 +1,14 @@
- -->
+
+