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 {
minSdk 24 minSdk 24
targetSdk 32 targetSdk 32
versionCode 12 versionCode 12
versionName "1.2" versionName "1.3"
resValue "string", "weathericons_version", "${versionName}" resValue "string", "weathericons_version", "${versionName}"

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

@ -46,6 +46,8 @@ public class BizGlobal {
public static final String EMPTY_PROJECT_TIPS = "先去添加打卡项吧~"; public static final String EMPTY_PROJECT_TIPS = "先去添加打卡项吧~";
public static final String EMPTY_PROJECT_TIPS_ICON = "cmd_alert_decagram_outline"; public static final String EMPTY_PROJECT_TIPS_ICON = "cmd_alert_decagram_outline";
public static final int REQUEST_CODE_GENERAL = 1; 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_FILE = "file";
private static final String URI_SCHEMA_CONTENT = "content"; 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 {
@Query("select * from t_record where deleted = 0 and create_date >= :startDate and create_date <= :endDate order by check_date desc, create_date desc") @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); Flowable<List<Record>> queryByDate(long startDate, long endDate);
@Insert
Completable insertAll(List<Record> records);
@Insert @Insert
Completable insertAll(Record... records); Completable insertAll(Record... records);

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

@ -1,6 +1,7 @@
package cc.niushuai.dididone.ui.setting; package cc.niushuai.dididone.ui.setting;
import android.Manifest; import android.Manifest;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
@ -15,6 +16,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@ -33,12 +35,17 @@ import cc.niushuai.dididone.ui.base.BaseFragment;
import cc.niushuai.dididone.util.GsonUtil; import cc.niushuai.dididone.util.GsonUtil;
import cc.niushuai.dididone.util.Toasts; import cc.niushuai.dididone.util.Toasts;
import cc.niushuai.dididone.util.XLog; 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.DatePattern;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import io.reactivex.Completable;
import io.reactivex.CompletableObserver;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
public class SettingFragment extends BaseFragment { public class SettingFragment extends BaseFragment {
@ -66,32 +73,38 @@ public class SettingFragment extends BaseFragment {
} }
private void addRestoreClickListener() { private void addRestoreClickListener() {
binding.sSetRestore.setOnClickListener(view -> {
requestForPermission(BizGlobal.REQUEST_CODE_RESTORE, Intent.ACTION_GET_CONTENT);
});
} }
private void addBackupClickListener() { private void addBackupClickListener() {
binding.sSetBackup.setOnClickListener(view -> { binding.sSetBackup.setOnClickListener(view -> {
if ( requestForPermission(BizGlobal.REQUEST_CODE_BACKUP, Intent.ACTION_CREATE_DOCUMENT);
// true表示未授权读权限 });
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED || }
// true表示未授权写权限
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { private void requestForPermission(Integer requestCode, String action) {
// 申请授权 if (
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) // true表示未授权读权限
|| ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED ||
) { // true表示未授权写权限
Toasts.longShow(getContext(), "您曾经选择过禁止弹窗授权, 请手动进入应用管理授权"); ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
} else { // 申请授权
ActivityCompat.requestPermissions(getActivity(), new String[]{ if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
Manifest.permission.READ_EXTERNAL_STORAGE, || ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
Manifest.permission.WRITE_EXTERNAL_STORAGE, ) {
}, BizGlobal.REQUEST_CODE_GENERAL); Toasts.longShow(getContext(), "您曾经选择过禁止弹窗授权, 请手动进入应用管理授权");
}
} else { } 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 @Override
@ -99,39 +112,47 @@ public class SettingFragment extends BaseFragment {
boolean isAllGranted = false; boolean isAllGranted = false;
switch (requestCode) {
case BizGlobal.REQUEST_CODE_GENERAL:
for (int i = 0; i < permissions.length; i++) { for (int i = 0; i < permissions.length; i++) {
XLog.d("permission: {} result: {}", permissions[i], grantResults[i]); XLog.d("permission: {} result: {}", permissions[i], grantResults[i]);
if (grantResults[i] == PackageManager.PERMISSION_DENIED) { if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
isAllGranted = false; isAllGranted = false;
// break; // break;
} }
}
break;
default:
XLog.d("un case requestCode: {}", requestCode);
} }
if (isAllGranted) { 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.addCategory(Intent.CATEGORY_OPENABLE);
chooseFileLocationIntent.setType("*/*"); 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中监听选择的文件 // 选择文件 在onActivityResult中监听选择的文件
startActivityForResult(chooseFileLocationIntent, BizGlobal.REQUEST_CODE_GENERAL); startActivityForResult(chooseFileLocationIntent, requestCode);
} }
private String dealBackupContent(List<Record> recordList) { private String dealBackupContent(List<Record> recordList) {
@ -144,33 +165,131 @@ public class SettingFragment extends BaseFragment {
projectRecordList.add(new ProjectRecord(allProjectMap.get(projectId), collect)); projectRecordList.add(new ProjectRecord(allProjectMap.get(projectId), collect));
} }
// JSON化 // JSON化
return GsonUtil.toJsonString(projectRecordList); return GsonUtil.toJson(projectRecordList);
} }
@Override @Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == BizGlobal.REQUEST_CODE_GENERAL && requestCode == 1) { if (resultCode != Activity.RESULT_OK) {
Uri fileUri = data.getData(); Toasts.shortShow(getContext(), "操作失败 {} --> {}", requestCode + "", resultCode + "");
return;
Single<List<Record>> listSingle = DBManager.INSTANCE.recordDao().xListAll(); }
listSingle.subscribeOn(Schedulers.io()) if (BizGlobal.REQUEST_CODE_BACKUP.equals(requestCode)) {
.observeOn(AndroidSchedulers.mainThread()) backupResult(data);
.subscribe((recordList, throwable) -> { } else if (BizGlobal.REQUEST_CODE_RESTORE.equals(requestCode)) {
XLog.d("addBackClickListener == " + recordList.size()); restoreResult(data);
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);
}
});
} }
} }
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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();

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

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

Loading…
Cancel
Save