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.
git1/src/java/net/micode/notes/tool/BackupUtils.java

528 lines
22 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)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// BackupUtils.java - 小米便签备份工具类
// 主要功能:将便签数据导出为文本文件格式,方便用户备份和查看
package net.micode.notes.tool;
// ======================= 导入区域 =======================
// Android系统相关类
import android.content.Context; // 上下文用于获取资源、ContentResolver等
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; // 资源文件R类
// 应用数据模型相关
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.DataColumns; // 数据表列定义
import net.micode.notes.data.Notes.DataConstants;// 数据常量定义
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
// Java IO相关
import java.io.File; // 文件操作
import java.io.FileNotFoundException; // 文件未找到异常
import java.io.FileOutputStream; // 文件输出流
import java.io.IOException; // IO异常
import java.io.PrintStream; // 打印输出流
// ======================= 备份工具主类 =======================
/**
* BackupUtils - 便签备份工具
* 采用单例设计模式,确保整个应用只有一个备份工具实例
* 主要功能:将数据库中的便签数据导出为可读的文本文件
*/
public class BackupUtils {
// TAG - 日志标签用于Logcat日志筛选
private static final String TAG = "BackupUtils";
// ======================= 单例模式实现 =======================
// sInstance - 静态单例实例volatile确保多线程可见性
private static BackupUtils sInstance;
/**
* 获取BackupUtils单例实例
* synchronized - 线程安全,防止多线程环境下创建多个实例
* @param context 应用上下文,用于初始化
* @return BackupUtils唯一实例
*/
public static synchronized BackupUtils getInstance(Context context) {
// 懒加载:首次调用时创建实例
if (sInstance == null) {
sInstance = new BackupUtils(context);
}
return sInstance;
}
// ======================= 备份状态常量定义 =======================
/**
* 备份/恢复操作结果状态码
* 使用常量而非魔法数字,提高代码可读性
*/
public static final int STATE_SD_CARD_UNMOUONTED = 0; // SD卡未挂载无法访问外部存储
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; // 操作成功完成
// mTextExport - 文本导出器实例,实际执行导出操作
private TextExport mTextExport;
/**
* 私有构造函数 - 单例模式关键,防止外部直接实例化
* @param context 应用上下文用于初始化TextExport
*/
private BackupUtils(Context context) {
// 创建文本导出器,传入上下文用于资源访问
mTextExport = new TextExport(context);
}
/**
* 检查外部存储状态
* @return true: SD卡已挂载可读写; false: SD卡不可用
*/
private static boolean externalStorageAvailable() {
// Environment.MEDIA_MOUNTED - 存储介质已挂载且可读写
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* 执行文本导出操作 - 对外暴露的主要接口
* @return 操作结果状态码(见上面的状态常量)
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
* 获取导出的文件名
* @return 导出的文本文件名
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
* 获取导出的文件目录
* @return 文件保存的目录路径
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
// ======================= 文本导出内部类 =======================
/**
* TextExport - 文本导出器内部类
* 私有静态内部类,封装具体的导出逻辑
* 负责:查询数据 -> 格式化 -> 写入文件
*/
private static class TextExport {
// ======================= 数据库查询配置 =======================
// NOTE_PROJECTION - 便签表查询字段投影
// 指定从数据库查询哪些字段,避免查询不必要的数据
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID, // 0 - 便签ID主键
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_PROJECTION - 便签数据表查询字段投影
// 便签具体内容存储在数据表中
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT, // 0 - 便签内容
DataColumns.MIME_TYPE, // 1 - 数据类型(普通便签/通话记录)
DataColumns.DATA1, // 2 - 扩展数据1通话记录时为通话时间
DataColumns.DATA2, // 3 - 扩展数据2
DataColumns.DATA3, // 4 - 扩展数据3
DataColumns.DATA4, // 5 - 扩展数据4通话记录时为电话号码
};
// 数据表字段索引常量
private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引
private static final int DATA_COLUMN_MIME_TYPE = 1; // 数据类型列索引
private static final int DATA_COLUMN_CALL_DATE = 2; // 通话日期列索引
private static final int DATA_COLUMN_PHONE_NUMBER = 4; // 电话号码列索引
// ======================= 文本格式化配置 =======================
// TEXT_FORMAT - 文本格式化模板数组
// 从strings.xml的format_for_exported_note数组加载
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; // 导出的文件目录
/**
* TextExport构造函数
* @param context 应用上下文,用于获取格式化模板
*/
public TextExport(Context context) {
// 从资源文件加载格式化模板
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
// 初始化为空字符串
mFileName = "";
mFileDirectory = "";
}
/**
* 获取指定索引的格式化模板
* @param id 模板索引FORMAT_FOLDER_NAME等
* @return 格式化字符串
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
* 导出指定文件夹下的所有便签
* 按文件夹组织导出,保持原有结构
* @param folderId 文件夹ID
* @param ps 打印输出流,用于写入文件
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询该文件夹下的所有便签
// Notes.CONTENT_NOTE_URI - 便签内容URI
// NoteColumns.PARENT_ID - 父文件夹ID条件
Cursor notesCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 便签表URI
NOTE_PROJECTION, // 查询的字段
NoteColumns.PARENT_ID + "=?", // 查询条件父ID等于指定文件夹
new String[] { folderId }, // 查询参数
null // 排序方式null表示默认
);
// 检查游标有效性
if (notesCursor != null) {
// 遍历查询结果
if (notesCursor.moveToFirst()) {
do {
// 1. 打印便签修改时间
// 使用格式化模板,将时间戳格式化为可读字符串
ps.println(String.format(
getFormat(FORMAT_NOTE_DATE),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm), // 日期时间格式
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE) // 时间戳
)
));
// 2. 导出该便签的具体内容
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) {
// 查询该便签的所有数据项
Cursor dataCursor = mContext.getContentResolver().query(
Notes.CONTENT_DATA_URI, // 便签数据表URI
DATA_PROJECTION, // 查询字段
DataColumns.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)) {
// ===== 通话记录类型 =====
// 1. 获取通话相关数据
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
// 2. 打印电话号码(如果有)
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
}
// 3. 打印通话时间
ps.println(String.format(
getFormat(FORMAT_NOTE_CONTENT),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
callDate
)
));
// 4. 打印位置信息(如果有)
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());
}
}
/**
* 导出所有便签到文本文件 - 主导出方法
* 导出流程:
* 1. 检查SD卡状态
* 2. 创建输出文件
* 3. 导出文件夹便签
* 4. 导出根目录便签
* 5. 关闭流
* @return 导出状态码
*/
public int exportToText() {
// 1. 检查SD卡是否可用
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
// 2. 获取输出流
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// 3. 导出文件夹及其便签
// 查询条件说明:
// - 类型为文件夹 且 不在回收站中
// - 或者 ID是通话记录文件夹
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 便签表
NOTE_PROJECTION, // 查询字段
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER,
null, // 查询参数
null // 排序
);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// 获取文件夹名称
String folderName = "";
// 判断是否为通话记录文件夹
if(folderCursor.getLong(NOTE_COLUMN_ID) == 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));
}
// 导出该文件夹下的所有便签
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// 4. 导出根目录下的便签(没有父文件夹的便签)
// 查询条件:类型为普通便签 且 父ID为0根目录
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_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)
)
));
// 导出便签内容
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
// 5. 关闭输出流
ps.close();
return STATE_SUCCESS;
}
/**
* 创建输出文件并获取打印流
* 文件路径SD卡根目录 + 配置路径 + 日期时间文件名
* @return PrintStream 输出流失败返回null
*/
private PrintStream getExportToTextPrintStream() {
// 生成输出文件
File file = generateFileMountedOnSDcard(
mContext,
R.string.file_path, // 文件路径资源ID
R.string.file_name_txt_format // 文件名格式资源ID
);
if (file == null) {
Log.e(TAG, "create file to exported failed");
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"/backup/"
* @param fileNameFormatResId 文件名格式资源ID"notes_%s.txt"
* @return 创建的文件对象失败返回null
*/
private static File generateFileMountedOnSDcard(
Context context,
int filePathResId,
int fileNameFormatResId
) {
// 使用StringBuilder构建文件路径提高性能
StringBuilder sb = new StringBuilder();
// 1. SD卡根目录
sb.append(Environment.getExternalStorageDirectory());
// 2. 应用指定的文件路径
sb.append(context.getString(filePathResId));
// 创建目录对象
File filedir = new File(sb.toString());
// 3. 添加文件名(使用当前日期格式化)
// 格式如notes_20231215.txt
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(
context.getString(R.string.format_date_ymd), // 日期格式
System.currentTimeMillis() // 当前时间戳
)
));
// 创建文件对象
File file = new File(sb.toString());
try {
// 创建目录(如果不存在)
if (!filedir.exists()) {
filedir.mkdir(); // 创建单级目录
}
// 创建文件(如果不存在)
if (!file.exists()) {
file.createNewFile();
}
return file;
} catch (SecurityException e) {
// 权限异常无SD卡写入权限
e.printStackTrace();
} catch (IOException e) {
// IO异常磁盘空间不足等
e.printStackTrace();
}
return null; // 创建失败返回null
}
}