Browse Source

feat: 💯 备份逻辑完成

master
niushuai233 1 year ago
parent
commit
7564cd3683
  1. 2
      app/build.gradle
  2. 3
      app/src/main/AndroidManifest.xml
  3. 4
      app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java
  4. 4
      app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java
  5. 32
      app/src/main/java/cc/niushuai/dididone/biz/entity/ProjectRecord.java
  6. 157
      app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java
  7. 116
      app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java
  8. 14
      app/src/main/res/layout/fragment_setting.xml
  9. 2
      app/src/main/res/navigation/mobile_navigation.xml
  10. 4
      app/src/main/res/values/dimens.xml
  11. 3
      app/src/main/res/values/strings.xml

2
app/build.gradle

@ -102,6 +102,8 @@ dependencies { @@ -102,6 +102,8 @@ dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// hutool
implementation 'cn.hutool:hutool-core:5.0.7'
// gson json
implementation 'com.google.code.gson:gson:2.10.1'
// room持久化库
def room_version = '2.6.1'

3
app/src/main/AndroidManifest.xml

@ -28,4 +28,7 @@ @@ -28,4 +28,7 @@
</activity>
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>

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

@ -45,7 +45,9 @@ public class BizGlobal { @@ -45,7 +45,9 @@ public class BizGlobal {
public static final Map<Long, Integer> CACHE_PROJECT_COUNT = new HashMap<>();
public static final String EMPTY_PROJECT_TIPS = "先去添加打卡项吧~";
public static final String EMPTY_PROJECT_TIPS_ICON = "cmd_alert_decagram_outline";
public static int REQUEST_CODE_GENERAL = 1;
public static final int REQUEST_CODE_GENERAL = 1;
private static final String URI_SCHEMA_FILE = "file";
private static final String URI_SCHEMA_CONTENT = "content";
private BizGlobal() {
}

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

@ -13,6 +13,7 @@ import cc.niushuai.dididone.biz.entity.Record; @@ -13,6 +13,7 @@ import cc.niushuai.dididone.biz.entity.Record;
import cc.niushuai.dididone.biz.vo.ProjectCount;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Single;
@Dao
public interface RecordDao {
@ -20,6 +21,9 @@ public interface RecordDao { @@ -20,6 +21,9 @@ public interface RecordDao {
@Query("SELECT * FROM t_record order by check_date desc, create_date desc")
Flowable<List<Record>> listAll();
@Query("SELECT * FROM t_record order by check_date asc, create_date desc")
Single<List<Record>> xListAll();
@Query("select * from t_record where deleted = 0 and check_date = :date order by check_date desc, create_date desc")
Flowable<List<Record>> queryByDate(long date);

32
app/src/main/java/cc/niushuai/dididone/biz/entity/ProjectRecord.java

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
package cc.niushuai.dididone.biz.entity;
import java.util.ArrayList;
import java.util.List;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
public class ProjectRecord extends Project {
private List<Record> recordList;
public ProjectRecord(Project project, List<Record> recordList) {
if (null == project) {
throw new IllegalArgumentException("project should not be null");
}
BeanUtil.copyProperties(project, this);
if (CollUtil.isEmpty(recordList)) {
this.recordList = new ArrayList<>(0);
} else {
this.recordList = recordList;
}
}
public List<Record> getRecordList() {
return recordList;
}
public void setRecordList(List<Record> recordList) {
this.recordList = recordList;
}
}

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

@ -1,19 +1,49 @@ @@ -1,19 +1,49 @@
package cc.niushuai.dididone.ui.setting;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import cc.niushuai.dididone.R;
import cc.niushuai.dididone.biz.BizGlobal;
import cc.niushuai.dididone.biz.entity.Project;
import cc.niushuai.dididone.biz.entity.ProjectRecord;
import cc.niushuai.dididone.biz.entity.Record;
import cc.niushuai.dididone.biz.roomx.DBManager;
import cc.niushuai.dididone.databinding.FragmentSettingBinding;
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.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class SettingFragment extends Fragment {
public class SettingFragment extends BaseFragment {
private static final String FILE_NAME = "{}_backup_{}.json";
private FragmentSettingBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
@ -22,33 +52,124 @@ public class SettingFragment extends Fragment { @@ -22,33 +52,124 @@ public class SettingFragment extends Fragment {
binding = FragmentSettingBinding.inflate(inflater, container, false);
View root = binding.getRoot();
initListeners();
return root;
}
private void initListeners() {
@Override
public void setListeners() {
// 打开icon列表activity
iconClickListener();
// 备份点击监听事件
addBackupClickListener();
// 恢复点击监听事件
addRestoreClickListener();
// 打开新建打卡项activity
projectClickListener();
}
private void iconClickListener() {
binding.sSetAppIcon.setOnClickListener(view -> {
XLog.d( "sSetAppIcon click");
private void addRestoreClickListener() {
}
XLog.d( "sSetAppIcon click complete");
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);
}
} else {
chooseBackupFileLocation();
}
});
}
private void projectClickListener() {
binding.sSetAppProject.setOnClickListener(view -> {
XLog.d( "sSetAppProject click");
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
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]);
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
isAllGranted = false;
// break;
}
}
break;
default:
XLog.d("un case requestCode: {}", requestCode);
}
if (isAllGranted) {
// 授权通过时 打开文件保存逻辑
chooseBackupFileLocation();
}
}
private void chooseBackupFileLocation() {
// 写文件
Intent chooseFileLocationIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
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)));
// 选择文件 在onActivityResult中监听选择的文件
startActivityForResult(chooseFileLocationIntent, BizGlobal.REQUEST_CODE_GENERAL);
}
private String dealBackupContent(List<Record> recordList) {
// 组装实体类
Map<Long, Project> allProjectMap = BizGlobal.getAllProjectMap();
List<ProjectRecord> projectRecordList = new ArrayList<>(allProjectMap.size());
for (Long projectId : allProjectMap.keySet()) {
List<Record> collect = recordList.stream().filter(item -> projectId.equals(item.getProjectId())).collect(Collectors.toList());
projectRecordList.add(new ProjectRecord(allProjectMap.get(projectId), collect));
}
// JSON化
return GsonUtil.toJsonString(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);
}
});
}
}
@Override
public void onDestroyView() {

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

@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
package cc.niushuai.dididone.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
public class GsonUtil {
//线程安全的
private static final Gson GSON;
private static final Gson GSON_NULL; // 不过滤空值
static {
GSON = new GsonBuilder().enableComplexMapKeySerialization() //当Map的key为复杂对象时,需要开启该方法
// .serializeNulls() //当字段值为空或null时,依然对该字段进行转换
// .excludeFieldsWithoutExposeAnnotation()//打开Export注解,但打开了这个注解,副作用,要转换和不转换都要加注解
.setDateFormat("yyyy-MM-dd HH:mm:ss")//序列化日期格式 "yyyy-MM-dd"
.setPrettyPrinting() //自动格式化换行
.disableHtmlEscaping() //防止特殊字符出现乱码
.create();
GSON_NULL = new GsonBuilder().enableComplexMapKeySerialization() //当Map的key为复杂对象时,需要开启该方法
.serializeNulls() //当字段值为空或null时,依然对该字段进行转换
// .excludeFieldsWithoutExposeAnnotation()//打开Export注解,但打开了这个注解,副作用,要转换和不转换都要加注解
.setDateFormat("yyyy-MM-dd HH:mm:ss")//序列化日期格式 "yyyy-MM-dd"
.setPrettyPrinting() //自动格式化换行
.disableHtmlEscaping() //防止特殊字符出现乱码
.create();
}
//获取gson解析器
public static Gson getGson() {
return GSON;
}
//获取gson解析器 有空值 解析
public static Gson getWriteNullGson() {
return GSON_NULL;
}
/**
* 根据对象返回json 过滤空值字段
*/
public static String toJsonStringIgnoreNull(Object object) {
return GSON.toJson(object);
}
/**
* 根据对象返回json 不过滤空值字段
*/
public static String toJsonString(Object object) {
return GSON_NULL.toJson(object);
}
/**
* 将字符串转化对象
*
* @param json 源字符串
* @param classOfT 目标对象类型
* @param <T>
* @return
*/
public static <T> T strToJavaBean(String json, Class<T> classOfT) {
return GSON.fromJson(json, classOfT);
}
/**
* 将json转化为对应的实体对象
* new TypeToken<List<T>>() {}.getType()
* new TypeToken<Map<String, T>>() {}.getType()
* new TypeToken<List<Map<String, T>>>() {}.getType()
*/
public static <T> T fromJson(String json, Type typeOfT) {
return GSON.fromJson(json, typeOfT);
}
/**
* 转成list
*
* @param gsonString
* @param cls
* @return
*/
public static <T> List<T> strToList(String gsonString, Class<T> cls) {
return GSON.fromJson(gsonString, new TypeToken<List<T>>() {
}.getType());
}
/**
* 转成list中有map的
*
* @param gsonString
* @return
*/
public static <T> List<Map<String, T>> strToListMaps(String gsonString) {
return GSON.fromJson(gsonString, new TypeToken<List<Map<String, String>>>() {
}.getType());
}
/**
* 转成map
*
* @param gsonString
* @return
*/
public static <T> Map<String, T> strToMaps(String gsonString) {
return GSON.fromJson(gsonString, new TypeToken<Map<String, T>>() {
}.getType());
}
}

14
app/src/main/res/layout/fragment_setting.xml

@ -17,13 +17,13 @@ @@ -17,13 +17,13 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="@string/s_set_app"
android:text="@string/s_set_backup_restore"
android:textAlignment="inherit"
android:textColor="#CE21D873"
android:textSize="30dp" />
<LinearLayout
android:id="@+id/s_set_app_icon"
android:id="@+id/s_set_backup"
android:layout_width="match_parent"
android:layout_height="@dimen/s_set_linear_layout_height"
android:layout_gravity="center_vertical"
@ -38,19 +38,19 @@ @@ -38,19 +38,19 @@
android:layout_marginLeft="@dimen/s_set_linear_image_view_marginLeft"
android:layout_marginTop="@dimen/s_set_linear_image_view_marginTop"
app:ico_color="@color/gray"
app:ico_icon="faw_laugh" />
app:ico_icon="cmd_cloud_download" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/s_set_linear_text_view_marginLeft"
android:gravity="left|center"
android:text="@string/s_set_icon"
android:text="@string/s_set_backup"
android:textSize="20dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/s_set_app_project"
android:id="@+id/s_set_restore"
android:layout_width="match_parent"
android:layout_height="@dimen/s_set_linear_layout_height"
android:layout_marginLeft="@dimen/s_set_linear_layout_marginLeft"
@ -64,14 +64,14 @@ @@ -64,14 +64,14 @@
android:layout_marginLeft="@dimen/s_set_linear_image_view_marginLeft"
android:layout_marginTop="@dimen/s_set_linear_image_view_marginTop"
app:ico_color="@color/gray"
app:ico_icon="cmd_cards_variant" />
app:ico_icon="cmd_cloud_upload" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/s_set_linear_text_view_marginLeft"
android:gravity="left|center"
android:text="@string/s_set_project"
android:text="@string/s_set_restore"
android:textSize="20dp" />
</LinearLayout>
</LinearLayout>

2
app/src/main/res/navigation/mobile_navigation.xml

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@id/n_nav_calendar">
app:startDestination="@id/n_nav_setting">
<fragment
android:id="@id/n_nav_calendar"

4
app/src/main/res/values/dimens.xml

@ -5,11 +5,11 @@ @@ -5,11 +5,11 @@
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">155dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="s_set_linear_layout_height">50dp</dimen>
<dimen name="s_set_linear_layout_height">75dp</dimen>
<dimen name="s_set_linear_layout_marginTop">10dp</dimen>
<dimen name="s_set_linear_layout_marginLeft">15dp</dimen>
<dimen name="s_set_linear_layout_marginRight">15dp</dimen>
<dimen name="s_set_linear_image_view_size">32dp</dimen>
<dimen name="s_set_linear_image_view_size">50dp</dimen>
<dimen name="s_set_linear_image_view_marginLeft">15dp</dimen>
<dimen name="s_set_linear_image_view_marginTop">10dp</dimen>
<dimen name="s_set_linear_text_view_marginLeft">10dp</dimen>

3
app/src/main/res/values/strings.xml

@ -18,6 +18,9 @@ @@ -18,6 +18,9 @@
<string name="n_menu_name_setting">设置</string>
<string name="s_set_target">目标</string>
<string name="s_set_app">App</string>
<string name="s_set_backup_restore">备份与恢复</string>
<string name="s_set_backup">备份</string>
<string name="s_set_restore">恢复</string>
<string name="s_set_icon">图标</string>
<string name="s_set_project">打卡项</string>
<string name="n_menu_name_icon">ICON</string>

Loading…
Cancel
Save