You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MiNote/tool/BackupUtils.java

419 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* 版权声明本文件由MiCode开源社区开发遵循Apache License, Version 2.0协议;
* 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取:
* http://www.apache.org/licenses/LICENSE-2.0
* 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。
*/
package net.micode.notes.tool;
import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
/**
* 笔记备份工具类
* 提供将笔记数据导出为文本文件的功能支持检查SD卡状态、生成备份文件、格式化导出内容等操作
*/
public class BackupUtils {
private static final String TAG = "BackupUtils";
// 单例实例(保证全局唯一)
private static BackupUtils sInstance;
/**
* 获取备份工具单例实例
* @param context 上下文环境
* @return BackupUtils单例对象
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new BackupUtils(context);
}
return sInstance;
}
/**
* 备份/恢复操作的状态常量(标识当前操作状态)
*/
// SD卡未挂载状态
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// 备份文件不存在状态
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// 数据格式损坏(可能被其他程序修改)
public static final int STATE_DATA_DESTROIED = 2;
// 系统运行时异常(导致备份/恢复失败)
public static final int STATE_SYSTEM_ERROR = 3;
// 备份/恢复成功状态
public static final int STATE_SUCCESS = 4;
// 文本导出功能模块实例
private TextExport mTextExport;
/**
* 私有构造方法(限制外部实例化)
* @param context 上下文环境
*/
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
/**
* 检查外部存储SD卡是否可用
* @return true-可用false-不可用
*/
private static boolean externalStorageAvailable() {
// 判断SD卡状态是否为"已挂载"
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* 触发文本导出操作
* @return 导出状态使用STATE_*常量)
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
* 获取导出的文本文件名
* @return 文件名(如"NotesBackup_20231025.txt"
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
* 获取导出文本文件的存储目录
* @return 文件目录路径(如"/sdcard/NotesBackup/"
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
* 内部类:文本导出功能实现
*/
private static class TextExport {
// 笔记查询投影指定需要查询的note表字段
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID, // 笔记ID索引0
NoteColumns.MODIFIED_DATE, // 最后修改时间索引1
NoteColumns.SNIPPET, // 笔记摘要索引2
NoteColumns.TYPE // 笔记类型索引3
};
private static final int NOTE_COLUMN_ID = 0; // 笔记ID列索引
private static final int NOTE_COLUMN_MODIFIED_DATE = 1; // 修改时间列索引
private static final int NOTE_COLUMN_SNIPPET = 2; // 摘要列索引
// 笔记关联数据查询投影指定需要查询的data表字段
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT, // 内容索引0
DataColumns.MIME_TYPE, // MIME类型索引1
DataColumns.DATA1, // 数据字段1通话记录日期索引2
DataColumns.DATA2, // 数据字段2预留索引3
DataColumns.DATA3, // 数据字段3预留索引4
DataColumns.DATA4, // 数据字段4电话号码索引5
};
private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引
private static final int DATA_COLUMN_MIME_TYPE = 1; // MIME类型列索引
private static final int DATA_COLUMN_CALL_DATE = 2; // 通话日期列索引DATA1
private static final int DATA_COLUMN_PHONE_NUMBER = 4; // 电话号码列索引DATA4
// 导出文本的格式数组(从资源文件读取,包含文件夹名、笔记日期、笔记内容的格式)
private final String [] TEXT_FORMAT;
private static final int FORMAT_FOLDER_NAME = 0; // 文件夹名格式索引
private static final int FORMAT_NOTE_DATE = 1; // 笔记日期格式索引
private static final int FORMAT_NOTE_CONTENT = 2; // 笔记内容格式索引
private Context mContext; // 上下文环境
private String mFileName; // 导出的文件名
private String mFileDirectory; // 导出文件的存储目录
/**
* 构造方法:初始化文本导出模块
* @param context 上下文环境
*/
public TextExport(Context context) {
// 从资源文件获取导出格式数组如R.array.format_for_exported_note
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
mFileName = "";
mFileDirectory = "";
}
/**
* 获取导出格式字符串(根据索引)
* @param id 格式数组索引使用FORMAT_*常量)
* @return 格式字符串(如"文件夹: %s"
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
* 将指定文件夹下的笔记导出到文本输出流
* @param folderId 文件夹ID
* @param ps 文本输出流(用于写入文件)
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询属于该文件夹的所有笔记note表
Cursor notesCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // note表的内容Uri
NOTE_PROJECTION, // 需要查询的字段
NoteColumns.PARENT_ID + "=?", // 查询条件父ID等于文件夹ID
new String[] { folderId }, // 查询参数
null // 排序方式(默认)
);
if (notesCursor != null) {
if (notesCursor.moveToFirst()) { // 移动到首条记录
do {
// 打印笔记的最后修改日期(使用指定格式)
ps.println(String.format(getFormat(FORMAT_NOTE_DATE),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm), // 日期时间格式(如"10月25日 15:30"
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE) // 笔记修改时间戳
)));
// 获取当前笔记ID导出该笔记的详细内容
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext()); // 遍历所有笔记
}
notesCursor.close(); // 关闭游标释放资源
}
}
/**
* 将指定笔记的详细内容导出到文本输出流
* @param noteId 笔记ID
* @param ps 文本输出流(用于写入文件)
*/
private void exportNoteToText(String noteId, PrintStream ps) {
// 查询该笔记关联的data表数据如文本内容、通话记录等
Cursor dataCursor = mContext.getContentResolver().query(
Notes.CONTENT_DATA_URI, // data表的内容Uri
DATA_PROJECTION, // 需要查询的字段
DataColumns.NOTE_ID + "=?", // 查询条件note_id等于当前笔记ID
new String[] { noteId }, // 查询参数
null // 排序方式(默认)
);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) { // 移动到首条记录
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); // 获取数据类型
if (DataConstants.CALL_NOTE.equals(mimeType)) { // 通话记录类型
// 提取通话记录信息
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); // 电话号码
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); // 通话时间戳
String location = dataCursor.getString(DATA_COLUMN_CONTENT); // 通话附件位置(如录音路径)
// 打印电话号码(非空时)
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
}
// 打印通话时间(格式化后)
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
DateFormat.format(mContext.getString(R.string.format_datetime_mdhm), callDate)));
// 打印通话附件位置(非空时)
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location));
}
} else if (DataConstants.NOTE.equals(mimeType)) { // 普通文本笔记类型
String content = dataCursor.getString(DATA_COLUMN_CONTENT); // 笔记内容
if (!TextUtils.isEmpty(content)) { // 内容非空时打印
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), content));
}
}
} while (dataCursor.moveToNext()); // 遍历所有关联数据
}
dataCursor.close(); // 关闭游标释放资源
}
// 在笔记之间插入换行分隔符
try {
ps.write(new byte[] { Character.LINE_SEPARATOR, Character.LETTER_NUMBER });
} catch (IOException e) {
Log.e(TAG, "写入换行符失败: " + e.toString());
}
}
/**
* 执行文本导出的核心方法(用户可调用的导出入口)
* @return 导出状态使用STATE_*常量)
*/
public int exportToText() {
// 检查SD卡是否已挂载
if (!externalStorageAvailable()) {
Log.d(TAG, "SD卡未挂载");
return STATE_SD_CARD_UNMOUONTED;
}
// 获取文本输出流(指向生成的备份文件)
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "获取输出流失败");
return STATE_SYSTEM_ERROR;
}
// 第一步:导出文件夹及其下的笔记
// 查询所有有效文件夹(普通文件夹或通话记录文件夹)
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // note表的内容Uri
NOTE_PROJECTION, // 需要查询的字段
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " // 类型为文件夹
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " // 非回收站文件夹
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, // 或通话记录文件夹
null, // 查询参数(无)
null // 排序方式(默认)
);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) { // 移动到首条记录
do {
// 获取文件夹名称(通话记录文件夹使用固定名称,其他文件夹使用摘要)
String folderName = "";
long folderId = folderCursor.getLong(NOTE_COLUMN_ID);
if (folderId == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name); // 通话记录文件夹名称(如"通话录音"
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); // 普通文件夹名称(摘要字段)
}
// 打印文件夹名称(非空时)
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
// 导出该文件夹下的所有笔记
exportFolderToText(String.valueOf(folderId), ps);
} while (folderCursor.moveToNext()); // 遍历所有文件夹
}
folderCursor.close(); // 关闭游标释放资源
}
// 第二步导出根目录下的笔记父ID为0的普通笔记
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // note表的内容Uri
NOTE_PROJECTION, // 需要查询的字段
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " // 类型为普通笔记
+ NoteColumns.PARENT_ID + "=0", // 父ID为0根目录
null, // 查询参数(无)
null // 排序方式(默认)
);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) { // 移动到首条记录
do {
// 打印笔记的最后修改日期
ps.println(String.format(getFormat(FORMAT_NOTE_DATE),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm), // 日期时间格式
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE) // 笔记修改时间戳
)));
// 获取当前笔记ID导出该笔记的详细内容
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext()); // 遍历所有根目录笔记
}
noteCursor.close(); // 关闭游标释放资源
}
ps.close(); // 关闭输出流
return STATE_SUCCESS; // 返回导出成功状态
}
/**
* 获取指向导出文件的打印流(用于写入文本内容)
* @return PrintStream输出流失败返回null
*/
private PrintStream getExportToTextPrintStream() {
// 生成SD卡上的导出文件路径和名称由资源文件定义
File file = generateFileMountedOnSDcard(
mContext,
R.string.file_path, // 文件存储目录的资源ID如"/NotesBackup/"
R.string.file_name_txt_format // 文件名格式的资源ID如"NotesBackup_%s.txt"
);
if (file == null) {
Log.e(TAG, "创建导出文件失败");
return null;
}
// 记录文件名和存储目录
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
// 创建文件输出流
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos); // 包装为打印流(支持格式化输出)
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (NullPointerException e) {
e.printStackTrace();
return null;
}
return ps;
}
}
/**
* 生成SD卡上的导出文件用于存储备份数据
* @param context 上下文环境
* @param filePathResId 存储目录的资源ID如R.string.file_path
* @param fileNameFormatResId 文件名格式的资源ID如R.string.file_name_txt_format
* @return 生成的File对象失败返回null
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
// 构建文件路径SD卡根目录 + 自定义目录 + 带时间戳的文件名
StringBuilder sb = new StringBuilder();
sb.append(Environment.getExternalStorageDirectory()); // SD卡根目录如"/storage/emulated/0"
sb.append(context.getString(filePathResId)); // 追加自定义目录(如"/NotesBackup/"
File filedir = new File(sb.toString()); // 目标目录对象
// 构建完整文件路径(目录 + 文件名)
sb.append(context.getString(
fileNameFormatResId, // 文件名格式(如"NotesBackup_%s.txt"
DateFormat.format(
context.getString(R.string.format_date_ymd), // 日期格式(如"20231025"
System.currentTimeMillis() // 当前时间戳
)
));
File file = new File(sb.toString()); // 目标文件对象
try {
// 创建目录(若不存在)
if (!filedir.exists()) {
filedir.mkdir(); // mkdir()仅创建单层目录如需多层用mkdirs()
}
// 创建文件(若不存在)
if (!file.exists()) {
file.createNewFile();
}
return file;
} catch (SecurityException e) {
// 权限不足异常如未获取SD卡写入权限
e.printStackTrace();
} catch (IOException e) {
// 文件创建失败异常
e.printStackTrace();
}
return null;
}
}