From 7564cd3683c0d2777470e76d977124fd6897e8ff Mon Sep 17 00:00:00 2001 From: niushuai233 Date: Mon, 22 Apr 2024 16:59:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:100:=20=E5=A4=87=E4=BB=BD=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 3 + .../cc/niushuai/dididone/biz/BizGlobal.java | 4 +- .../niushuai/dididone/biz/dao/RecordDao.java | 4 + .../dididone/biz/entity/ProjectRecord.java | 32 ++++ .../dididone/ui/setting/SettingFragment.java | 159 +++++++++++++++--- .../cc/niushuai/dididone/util/GsonUtil.java | 116 +++++++++++++ app/src/main/res/layout/fragment_setting.xml | 14 +- .../main/res/navigation/mobile_navigation.xml | 2 +- app/src/main/res/values/dimens.xml | 4 +- app/src/main/res/values/strings.xml | 3 + 11 files changed, 313 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/cc/niushuai/dididone/biz/entity/ProjectRecord.java create mode 100644 app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java diff --git a/app/build.gradle b/app/build.gradle index cf77365..5b86437 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7124e6a..d884415 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,4 +28,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java b/app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java index 0d6c080..53f5dce 100644 --- a/app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java +++ b/app/src/main/java/cc/niushuai/dididone/biz/BizGlobal.java @@ -45,7 +45,9 @@ public class BizGlobal { public static final Map 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() { } diff --git a/app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java b/app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java index 7dd23c0..3f3f730 100644 --- a/app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java +++ b/app/src/main/java/cc/niushuai/dididone/biz/dao/RecordDao.java @@ -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 { @Query("SELECT * FROM t_record order by check_date desc, create_date desc") Flowable> listAll(); + @Query("SELECT * FROM t_record order by check_date asc, create_date desc") + Single> xListAll(); + @Query("select * from t_record where deleted = 0 and check_date = :date order by check_date desc, create_date desc") Flowable> queryByDate(long date); diff --git a/app/src/main/java/cc/niushuai/dididone/biz/entity/ProjectRecord.java b/app/src/main/java/cc/niushuai/dididone/biz/entity/ProjectRecord.java new file mode 100644 index 0000000..6bf9a46 --- /dev/null +++ b/app/src/main/java/cc/niushuai/dididone/biz/entity/ProjectRecord.java @@ -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 recordList; + public ProjectRecord(Project project, List 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 getRecordList() { + return recordList; + } + + public void setRecordList(List recordList) { + this.recordList = recordList; + } +} diff --git a/app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java b/app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java index bc86f80..83d39dc 100644 --- a/app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java +++ b/app/src/main/java/cc/niushuai/dididone/ui/setting/SettingFragment.java @@ -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,34 +52,125 @@ 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 projectClickListener() { - binding.sSetAppProject.setOnClickListener(view -> { - XLog.d( "sSetAppProject click"); + 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(); + } }); } + @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 recordList) { + + // 组装实体类 + Map allProjectMap = BizGlobal.getAllProjectMap(); + List projectRecordList = new ArrayList<>(allProjectMap.size()); + for (Long projectId : allProjectMap.keySet()) { + List 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> 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(); diff --git a/app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java b/app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java new file mode 100644 index 0000000..5a2386f --- /dev/null +++ b/app/src/main/java/cc/niushuai/dididone/util/GsonUtil.java @@ -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 + * @return + */ + public static T strToJavaBean(String json, Class classOfT) { + return GSON.fromJson(json, classOfT); + } + + /** + * 将json转化为对应的实体对象 + * new TypeToken>() {}.getType() + * new TypeToken>() {}.getType() + * new TypeToken>>() {}.getType() + */ + public static T fromJson(String json, Type typeOfT) { + return GSON.fromJson(json, typeOfT); + } + + /** + * 转成list + * + * @param gsonString + * @param cls + * @return + */ + public static List strToList(String gsonString, Class cls) { + return GSON.fromJson(gsonString, new TypeToken>() { + }.getType()); + } + + /** + * 转成list中有map的 + * + * @param gsonString + * @return + */ + public static List> strToListMaps(String gsonString) { + return GSON.fromJson(gsonString, new TypeToken>>() { + }.getType()); + } + + /** + * 转成map + * + * @param gsonString + * @return + */ + public static Map strToMaps(String gsonString) { + return GSON.fromJson(gsonString, new TypeToken>() { + }.getType()); + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_setting.xml b/app/src/main/res/layout/fragment_setting.xml index 64c6759..9f9d818 100644 --- a/app/src/main/res/layout/fragment_setting.xml +++ b/app/src/main/res/layout/fragment_setting.xml @@ -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" /> + app:ico_icon="cmd_cloud_download" /> + app:ico_icon="cmd_cloud_upload" /> diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 47679f0..b946f35 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -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"> 8dp 155dp 16dp - 50dp + 75dp 10dp 15dp 15dp - 32dp + 50dp 15dp 10dp 10dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b525e8..7be9576 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,9 @@ 设置 目标 App + 备份与恢复 + 备份 + 恢复 图标 打卡项 ICON