/* * 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; } }