Browse Source

feat: 💯 备份与恢复

master
niushuai233 1 year ago
parent
commit
27e5b4055c
  1. 2
      app/build.gradle
  2. 2
      app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java
  3. 3
      app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java
  4. 231
      app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java
  5. 12
      app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java

2
app/build.gradle

@ -16,7 +16,7 @@ android { @@ -16,7 +16,7 @@ android {
minSdk 24
targetSdk 32
versionCode 12
versionName "1.2"
versionName "1.3"
resValue "string", "weathericons_version", "${versionName}"

2
app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java

@ -46,6 +46,8 @@ public class BizGlobal { @@ -46,6 +46,8 @@ public class BizGlobal {
public static final String EMPTY_PROJECT_TIPS = "先去添加打卡项吧~";
public static final String EMPTY_PROJECT_TIPS_ICON = "cmd_alert_decagram_outline";
public static final int REQUEST_CODE_GENERAL = 1;
public static final Integer REQUEST_CODE_BACKUP = 10005;
public static final Integer REQUEST_CODE_RESTORE = 10006;
private static final String URI_SCHEMA_FILE = "file";
private static final String URI_SCHEMA_CONTENT = "content";

3
app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java

@ -30,6 +30,9 @@ public interface RecordDao { @@ -30,6 +30,9 @@ public interface RecordDao {
@Query("select * from t_record where deleted = 0 and create_date >= :startDate and create_date <= :endDate order by check_date desc, create_date desc")
Flowable<List<Record>> queryByDate(long startDate, long endDate);
@Insert
Completable insertAll(List<Record> records);
@Insert
Completable insertAll(Record... records);

231
app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
package cc.niushuai.dididone.ui.setting;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@ -15,6 +16,7 @@ import androidx.core.app.ActivityCompat; @@ -15,6 +16,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -33,12 +35,17 @@ import cc.niushuai.dididone.ui.base.BaseFragment; @@ -33,12 +35,17 @@ import cc.niushuai.dididone.ui.base.BaseFragment;
import cc.niushuai.dididone.util.GsonUtil;
import cc.niushuai.dididone.util.Toasts;
import cc.niushuai.dididone.util.XLog;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import io.reactivex.Completable;
import io.reactivex.CompletableObserver;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class SettingFragment extends BaseFragment {
@ -66,32 +73,38 @@ public class SettingFragment extends BaseFragment { @@ -66,32 +73,38 @@ public class SettingFragment extends BaseFragment {
}
private void addRestoreClickListener() {
binding.sSetRestore.setOnClickListener(view -> {
requestForPermission(BizGlobal.REQUEST_CODE_RESTORE, Intent.ACTION_GET_CONTENT);
});
}
private void addBackupClickListener() {
binding.sSetBackup.setOnClickListener(view -> {
if (
// true表示未授权读权限
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED ||
// true表示未授权写权限
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
// 申请授权
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
|| ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
Toasts.longShow(getContext(), "您曾经选择过禁止弹窗授权, 请手动进入应用管理授权");
} else {
ActivityCompat.requestPermissions(getActivity(), new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
}, BizGlobal.REQUEST_CODE_GENERAL);
}
requestForPermission(BizGlobal.REQUEST_CODE_BACKUP, Intent.ACTION_CREATE_DOCUMENT);
});
}
private void requestForPermission(Integer requestCode, String action) {
if (
// true表示未授权读权限
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED ||
// true表示未授权写权限
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
// 申请授权
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
|| ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
Toasts.longShow(getContext(), "您曾经选择过禁止弹窗授权, 请手动进入应用管理授权");
} else {
chooseBackupFileLocation();
ActivityCompat.requestPermissions(getActivity(), new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
}, BizGlobal.REQUEST_CODE_GENERAL);
}
});
} else {
chooseBackupFileLocation(requestCode, action);
}
}
@Override
@ -99,39 +112,47 @@ public class SettingFragment extends BaseFragment { @@ -99,39 +112,47 @@ public class SettingFragment extends BaseFragment {
boolean isAllGranted = false;
switch (requestCode) {
case BizGlobal.REQUEST_CODE_GENERAL:
for (int i = 0; i < permissions.length; i++) {
XLog.d("permission: {} result: {}", permissions[i], grantResults[i]);
for (int i = 0; i < permissions.length; i++) {
XLog.d("permission: {} result: {}", permissions[i], grantResults[i]);
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
isAllGranted = false;
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
isAllGranted = false;
// break;
}
}
break;
default:
XLog.d("un case requestCode: {}", requestCode);
}
}
if (isAllGranted) {
// 授权通过时 打开文件保存逻辑
chooseBackupFileLocation();
String action = null;
if (BizGlobal.REQUEST_CODE_BACKUP.equals(requestCode)) {
action = Intent.ACTION_CREATE_DOCUMENT;
} else if (BizGlobal.REQUEST_CODE_RESTORE.equals(requestCode)) {
action = Intent.ACTION_GET_CONTENT;
} else {
XLog.d("unknown request code: {}", requestCode);
Toasts.shortShow(getContext(), "未知的请求码: {}", requestCode + "");
return;
}
chooseBackupFileLocation(requestCode, action);
}
}
private void chooseBackupFileLocation() {
private void chooseBackupFileLocation(Integer requestCode, String action) {
// 写文件
Intent chooseFileLocationIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
Intent chooseFileLocationIntent = new Intent(action);
chooseFileLocationIntent.addCategory(Intent.CATEGORY_OPENABLE);
chooseFileLocationIntent.setType("*/*");
chooseFileLocationIntent.putExtra(Intent.EXTRA_TITLE, StrUtil.format(FILE_NAME, getString(R.string.app_name), DateUtil.date().toString(DatePattern.PURE_DATETIME_PATTERN)));
if (BizGlobal.REQUEST_CODE_BACKUP.equals(requestCode)) {
chooseFileLocationIntent.putExtra(Intent.EXTRA_TITLE, StrUtil.format(FILE_NAME, getString(R.string.app_name), DateUtil.date().toString(DatePattern.PURE_DATETIME_PATTERN)));
}
// 选择文件 在onActivityResult中监听选择的文件
startActivityForResult(chooseFileLocationIntent, BizGlobal.REQUEST_CODE_GENERAL);
startActivityForResult(chooseFileLocationIntent, requestCode);
}
private String dealBackupContent(List<Record> recordList) {
@ -144,33 +165,131 @@ public class SettingFragment extends BaseFragment { @@ -144,33 +165,131 @@ public class SettingFragment extends BaseFragment {
projectRecordList.add(new ProjectRecord(allProjectMap.get(projectId), collect));
}
// JSON化
return GsonUtil.toJsonString(projectRecordList);
return GsonUtil.toJson(projectRecordList);
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == BizGlobal.REQUEST_CODE_GENERAL && requestCode == 1) {
Uri fileUri = data.getData();
Single<List<Record>> listSingle = DBManager.INSTANCE.recordDao().xListAll();
listSingle.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((recordList, throwable) -> {
XLog.d("addBackClickListener == " + recordList.size());
try {
String content = dealBackupContent(recordList);
OutputStream outputStream = getContext().getContentResolver().openOutputStream(fileUri);
IoUtil.write(outputStream, true, content.getBytes(StandardCharsets.UTF_8));
Toasts.shortShow(getContext(), "文件已备份: {}", fileUri.getPath().substring(fileUri.getPath().indexOf(":")).replace(":", "/sdcard/"));
} catch (FileNotFoundException e) {
XLog.d("save file【{}】 error: {}", fileUri.getPath(), e.getMessage(), e);
}
});
if (resultCode != Activity.RESULT_OK) {
Toasts.shortShow(getContext(), "操作失败 {} --> {}", requestCode + "", resultCode + "");
return;
}
if (BizGlobal.REQUEST_CODE_BACKUP.equals(requestCode)) {
backupResult(data);
} else if (BizGlobal.REQUEST_CODE_RESTORE.equals(requestCode)) {
restoreResult(data);
}
}
private void restoreResult(Intent data) {
try {
Uri uri = data.getData();
InputStream inputStream = getContext().getContentResolver().openInputStream(uri);
String readStr = IoUtil.read(inputStream, StandardCharsets.UTF_8);
List<Map<String, Object>> list = GsonUtil.toBean(readStr, List.class);
if (CollUtil.isEmpty(list)) {
Toasts.shortShow(getContext(), "未发现可导入内容, 请检查文件是否正确");
return;
}
List<ProjectRecord> projectRecordList = new ArrayList<>(list.size());
for (Map<String, Object> map : list) {
String json = GsonUtil.toJson(map);
ProjectRecord projectRecord = GsonUtil.toBean(json, ProjectRecord.class);
projectRecordList.add(projectRecord);
}
restoreProjectRecord(projectRecordList);
} catch (FileNotFoundException e) {
XLog.d("文件读取失败: {}", e.getMessage(), e);
Toasts.shortShow(getContext(), "文件读取失败: {}", e.getMessage());
}
}
private void restoreProjectRecord(List<ProjectRecord> projectRecordList) {
if (CollUtil.isNotEmpty(projectRecordList)) {
for (ProjectRecord projectRecord : projectRecordList) {
List<Record> recordList = projectRecord.getRecordList();
if (CollUtil.isNotEmpty(recordList)) {
// 插入record
Completable completable = DBManager.INSTANCE.recordDao().insertAll(recordList);
completable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
@Override
public void onSubscribe(Disposable d) {
XLog.d("restore onSubscribe");
}
@Override
public void onComplete() {
XLog.d("restore onComplete");
BizGlobal.buildCache();
}
@Override
public void onError(Throwable e) {
XLog.d("restore onError: {}", e.getMessage(), e);
}
});
}
// 插入project
Project project = new Project();
BeanUtil.copyProperties(projectRecord, project);
Completable completable = DBManager.INSTANCE.projectDao().insertAll(project);
completable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onComplete() {
BizGlobal.buildCache();
}
@Override
public void onError(Throwable e) {
}
});
}
}
}
private void backupResult(@NonNull Intent data) {
Uri fileUri = data.getData();
Single<List<Record>> listSingle = DBManager.INSTANCE.recordDao().xListAll();
listSingle.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((recordList, throwable) -> {
XLog.d("addBackClickListener == " + recordList.size());
try {
String content = dealBackupContent(recordList);
OutputStream outputStream = getContext().getContentResolver().openOutputStream(fileUri);
IoUtil.write(outputStream, true, content.getBytes(StandardCharsets.UTF_8));
Toasts.shortShow(getContext(), "文件已备份: {}", fileUri.getPath().substring(fileUri.getPath().indexOf(":")).replace(":", "/sdcard/"));
} catch (FileNotFoundException e) {
XLog.d("save file【{}】 error: {}", fileUri.getPath(), e.getMessage(), e);
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();

12
app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java

@ -45,14 +45,14 @@ public class GsonUtil { @@ -45,14 +45,14 @@ public class GsonUtil {
/**
* 根据对象返回json 过滤空值字段
*/
public static String toJsonStringIgnoreNull(Object object) {
public static String toJsonIgnoreNull(Object object) {
return GSON.toJson(object);
}
/**
* 根据对象返回json 不过滤空值字段
*/
public static String toJsonString(Object object) {
public static String toJson(Object object) {
return GSON_NULL.toJson(object);
}
@ -65,7 +65,7 @@ public class GsonUtil { @@ -65,7 +65,7 @@ public class GsonUtil {
* @param <T>
* @return
*/
public static <T> T strToJavaBean(String json, Class<T> classOfT) {
public static <T> T toBean(String json, Class<T> classOfT) {
return GSON.fromJson(json, classOfT);
}
@ -86,7 +86,7 @@ public class GsonUtil { @@ -86,7 +86,7 @@ public class GsonUtil {
* @param cls
* @return
*/
public static <T> List<T> strToList(String gsonString, Class<T> cls) {
public static <T> List<T> toList(String gsonString, Class<T> cls) {
return GSON.fromJson(gsonString, new TypeToken<List<T>>() {
}.getType());
}
@ -97,7 +97,7 @@ public class GsonUtil { @@ -97,7 +97,7 @@ public class GsonUtil {
* @param gsonString
* @return
*/
public static <T> List<Map<String, T>> strToListMaps(String gsonString) {
public static <T> List<Map<String, T>> toListMap(String gsonString) {
return GSON.fromJson(gsonString, new TypeToken<List<Map<String, String>>>() {
}.getType());
}
@ -108,7 +108,7 @@ public class GsonUtil { @@ -108,7 +108,7 @@ public class GsonUtil {
* @param gsonString
* @return
*/
public static <T> Map<String, T> strToMaps(String gsonString) {
public static <T> Map<String, T> toMap(String gsonString) {
return GSON.fromJson(gsonString, new TypeToken<Map<String, T>>() {
}.getType());
}

Loading…
Cancel
Save