/* * 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. */ 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; public class BackupUtils { private static final String TAG = "BackupUtils"; // Singleton stuff private static BackupUtils sInstance; public static synchronized BackupUtils getInstance(Context context) { if (sInstance == null) { sInstance = new BackupUtils(context); } return sInstance; } /** * Following states are signs to represents backup or restore * status */ // Currently, the sdcard is not mounted public static final int STATE_SD_CARD_UNMOUONTED = 0; // The backup file not exist public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // The data is not well formated, may be changed by other programs public static final int STATE_DATA_DESTROIED = 2; // Some run-time exception which causes restore or backup fails public static final int STATE_SYSTEM_ERROR = 3; // Backup or restore success public static final int STATE_SUCCESS = 4; private TextExport mTextExport; private BackupUtils(Context context) { mTextExport = new TextExport(context); } private static boolean externalStorageAvailable() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } public int exportToText() { return mTextExport.exportToText(); } public String getExportedTextFileName() { return mTextExport.mFileName; } public String getExportedTextFileDir() { return mTextExport.mFileDirectory; } private static class TextExport { private static final String[] NOTE_PROJECTION = { NoteColumns.ID, NoteColumns.MODIFIED_DATE, NoteColumns.SNIPPET, NoteColumns.TYPE }; private static final int NOTE_COLUMN_ID = 0; private static final int NOTE_COLUMN_MODIFIED_DATE = 1; private static final int NOTE_COLUMN_SNIPPET = 2; private static final String[] DATA_PROJECTION = { DataColumns.CONTENT, DataColumns.MIME_TYPE, DataColumns.DATA1, DataColumns.DATA2, DataColumns.DATA3, DataColumns.DATA4, }; 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; 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; public TextExport(Context context) { TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); mContext = context; mFileName = ""; mFileDirectory = ""; } private String getFormat(int id) { return TEXT_FORMAT[id]; } /** * Export the folder identified by folder id to text */ private void exportFolderToText(String folderId, PrintStream ps) { // Query notes belong to this folder Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { folderId }, null); if (notesCursor != null) { if (notesCursor.moveToFirst()) { do { // Print note's last modified date ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( mContext.getString(R.string.format_datetime_mdhm), notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); // Query data belong to this note String noteId = notesCursor.getString(NOTE_COLUMN_ID); exportNoteToText(noteId, ps); } while (notesCursor.moveToNext()); } notesCursor.close(); } } /** * Export note identified by id to a print stream */ private void exportNoteToText(String noteId, PrintStream ps) { Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, DataColumns.NOTE_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)) { // Print phone number 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)); } // Print call date ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat .format(mContext.getString(R.string.format_datetime_mdhm), callDate))); // Print call attachment location 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(); } // print a line separator between note try { ps.write(new byte[] { Character.LINE_SEPARATOR, Character.LETTER_NUMBER }); } catch (IOException e) { Log.e(TAG, e.toString()); } } /** * 该方法用于将笔记相关内容导出为用户可读的文本格式,会进行诸如检查外部存储是否可用、获取输出流、查询并处理文件夹及笔记数据等操作,最后返回表示导出操作结果的状态码。 */ public int exportToText() { // 调用externalStorageAvailable方法(此处未展示其实现,推测是用于检查外部存储是否可用的方法),如果外部存储不可用,执行以下逻辑。 if (!externalStorageAvailable()) { // 记录调试日志,提示“Media was not mounted”,表示外部存储介质(如SD卡等)未挂载,然后返回STATE_SD_CARD_UNMOUONTED(应该是预定义的表示SD卡未挂载的状态码常量),告知导出操作因存储问题失败。 Log.d(TAG, "Media was not mounted"); return STATE_SD_CARD_UNMOUONTED; } // 调用getExportToTextPrintStream方法(下方有其具体实现)尝试获取一个用于将内容输出到文本文件的打印流对象,如果获取失败(返回null),执行以下逻辑。 PrintStream ps = getExportToTextPrintStream(); if (ps == null) { // 记录错误日志,提示“get print stream error”,表示获取打印流出现错误,然后返回STATE_SYSTEM_ERROR(应该是预定义的表示系统错误的状态码常量),告知导出操作因获取输出流问题失败。 Log.e(TAG, "get print stream error"); return STATE_SYSTEM_ERROR; } // 首先导出文件夹及其包含的笔记信息 // 使用mContext(应该是上下文对象,用于获取内容解析器等操作与安卓系统交互)的getContentResolver方法获取内容解析器,并调用其query方法发起数据库查询操作,查询的URI为Notes.CONTENT_NOTE_URI(可能是预定义的表示笔记相关内容的通用URI)。 // 查询的字段列表为NOTE_PROJECTION(应该是预定义的表示要获取的具体字段的数组常量),筛选条件通过SQL语句指定,筛选出类型为文件夹(NoteColumns.TYPE字段等于Notes.TYPE_FOLDER)并且父文件夹ID不是回收站文件夹ID(NoteColumns.PARENT_ID字段不等于Notes.ID_TRASH_FOLER)或者ID等于通话记录文件夹ID(NoteColumns.ID字段等于Notes.ID_CALL_RECORD_FOLDER)的记录,最后两个参数为null,表示无排序等额外条件。 Cursor folderCursor = mContext.getContentResolver().query( Notes.CONTENT_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); // 判断游标对象folderCursor是否获取成功(不为null),如果获取成功,说明查询操作至少在获取游标这一步是正常执行的,可以尝试遍历游标获取查询结果数据并进行处理。 if (folderCursor!= null) { // 将游标移动到第一条数据位置,准备开始遍历游标获取数据。 if (folderCursor.moveToFirst()) { // 使用do-while循环遍历游标中的所有数据记录,每次循环处理一条记录,直到游标移动到最后一条记录后无法再移动(moveToNext返回false)为止。 do { // 初始化一个空字符串用于存储文件夹名称,后续会根据不同情况赋值。 String folderName = ""; // 判断当前游标指向的记录的ID(通过folderCursor.getLong(NOTE_COLUMN_ID)获取,NOTE_COLUMN_ID应该是预定义的表示ID所在列索引的常量)是否等于通话记录文件夹ID(Notes.ID_CALL_RECORD_FOLDER),如果相等,执行以下逻辑。 if (folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { // 通过mContext.getString方法(用于获取字符串资源)获取预定义的通话记录文件夹名称字符串资源(R.string.call_record_folder_name),并赋值给folderName变量,作为通话记录文件夹的名称。 folderName = mContext.getString(R.string.call_record_folder_name); } else { // 如果不是通话记录文件夹ID,通过folderCursor.getString方法(用于从游标获取字符串类型数据)获取记录中的文件夹名称字段(NOTE_COLUMN_SNIPPET应该是预定义的表示文件夹名称所在列索引的常量)的值,并赋值给folderName变量,获取普通文件夹的名称。 folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); } // 判断folderName是否不为空字符串(即通过TextUtils.isEmpty方法判断是否有有效的文件夹名称),如果不为空,执行以下逻辑。 if (!TextUtils.isEmpty(folderName)) { // 使用ps.println方法(通过前面获取的打印流对象ps将内容输出到文本文件中,println表示输出一行文本并换行)按照指定的格式(通过getFormat方法获取FORMAT_FOLDER_NAME对应的格式字符串,此处getFormat方法实现未展示)输出文件夹名称,将folderName变量的值按照格式要求格式化后输出到文本文件中。 ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); } // 通过folderCursor.getString方法获取当前记录的文件夹ID(NOTE_COLUMN_ID所在列的数据),并赋值给folderId变量,用于后续导出该文件夹下的笔记信息。 String folderId = folderCursor.getString(NOTE_COLUMN_ID); // 调用exportFolderToText方法(此处未展示其实现,推测是用于将指定文件夹及其内容导出为文本的方法),传入获取的folderId和打印流对象ps,将该文件夹及其包含的笔记信息导出到文本文件中。 exportFolderToText(folderId, ps); } while (folderCursor.moveToNext()); } // 无论是否成功遍历完所有数据记录,都要关闭游标对象folderCursor,释放相关的系统资源(如数据库连接等资源),避免资源泄漏。 folderCursor.close(); } // 导出根文件夹下的笔记信息 // 使用mContext的getContentResolver方法获取内容解析器,并调用其query方法发起数据库查询操作,查询的URI为Notes.CONTENT_NOTE_URI,查询的字段列表为NOTE_PROJECTION,筛选条件通过SQL语句指定,筛选出类型为笔记(NoteColumns.TYPE字段等于Notes.TYPE_NOTE)并且父文件夹ID为0(即根文件夹,NoteColumns.PARENT_ID字段等于0)的记录,最后两个参数为null,表示无排序等额外条件。 Cursor noteCursor = mContext.getContentResolver().query( Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "=0", null, null); // 判断游标对象noteCursor是否获取成功(不为null),如果获取成功,说明查询操作至少在获取游标这一步是正常执行的,可以尝试遍历游标获取查询结果数据并进行处理。 if (noteCursor!= null) { // 将游标移动到第一条数据位置,准备开始遍历游标获取数据。 if (noteCursor.moveToFirst()) { // 使用do-while循环遍历游标中的所有数据记录,每次循环处理一条记录,直到游标移动到最后一条记录后无法再移动(moveToNext返回false)为止。 do { // 使用ps.println方法按照指定的格式(通过getFormat方法获取FORMAT_NOTE_DATE对应的格式字符串,此处getFormat方法实现未展示)输出笔记的修改日期信息, // 先通过DateFormat.format方法(用于按照指定格式格式化日期)将从游标中获取的笔记修改日期(noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)获取,NOTE_COLUMN_MODIFIED_DATE应该是预定义的表示修改日期所在列索引的常量)按照预定义的日期时间格式(mContext.getString(R.string.format_datetime_mdhm)获取的格式字符串)进行格式化,然后将格式化后的日期字符串按照FORMAT_NOTE_DATE格式要求输出到文本文件中。 ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( mContext.getString(R.string.format_datetime_mdhm), noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); // 通过noteCursor.getString方法获取当前记录的笔记ID(NOTE_COLUMN_ID所在列的数据),并赋值给noteId变量,用于后续导出该笔记的详细信息。 String noteId = noteCursor.getString(NOTE_COLUMN_ID); // 调用exportNoteToText方法(此处未展示其实现,推测是用于将指定笔记导出为文本的方法),传入获取的noteId和打印流对象ps,将该笔记的详细信息导出到文本文件中。 exportNoteToText(noteId, ps); } while (noteCursor.moveToNext()); } // 无论是否成功遍历完所有数据记录,都要关闭游标对象noteCursor,释放相关的系统资源(如数据库连接等资源),避免资源泄漏。 noteCursor.close(); } // 关闭打印流对象ps,释放相关资源,完成整个文本导出操作,此时文本文件中应该已经包含了按照格式要求输出的文件夹、笔记等相关信息。 ps.close(); // 如果整个导出过程顺利完成,没有出现上述提到的各种错误情况,返回STATE_SUCCESS(应该是预定义的表示操作成功的状态码常量),告知导出操作成功。 return STATE_SUCCESS; } /** * 获取一个指向要导出文本文件的打印流对象,会先尝试创建对应的文件,如果文件创建失败或者获取输出流出现问题则返回null。 */ private PrintStream getExportToTextPrintStream() { // 调用generateFileMountedOnSDcard方法(下方有其具体实现)尝试创建一个挂载在SD卡上的文件(用于存储导出的文本内容),传入上下文对象mContext以及文件路径和文件名相关的字符串资源ID(R.string.file_path和R.string.file_name_txt_format,其具体内容应该是在资源文件中定义的路径和文件名格式相关字符串),获取对应的文件对象,如果获取失败(返回null),执行以下逻辑。 File file = generateFileMountedOnSDcard(mContext, R.string.file_path, R.string.file_name_txt_format); if (file == null) { // 记录错误日志,提示“create file to exported failed”,表示创建用于导出的文件失败,然后返回null,告知调用者获取打印流失败。 Log.e(TAG, "create file to exported failed"); return null; } // 将创建成功的文件的名称赋值给mFileName变量(可能用于后续记录等相关用途,此处具体作用取决于整体业务逻辑)。 mFileName = file.getName(); // 通过mContext.getString方法获取文件路径相关的字符串资源(R.string.file_path对应的字符串),并赋值给mFileDirectory变量(可能用于后续记录等相关用途,此处具体作用取决于整体业务逻辑)。 mFileDirectory = mContext.getString(R.string.file_path); PrintStream ps = null; try { // 创建一个基于前面创建的文件对象file的文件输出流对象fos,用于后续创建打印流对象并向文件中写入数据。 FileOutputStream fos = new FileOutputStream(file); // 使用创建的文件输出流对象fos创建一个打印流对象ps,通过打印流对象可以方便地按照文本格式将数据输出到文件中。 ps = new PrintStream(fos); } catch (FileNotFoundException e) { // 如果在创建文件输出流或者打印流过程中出现文件未找到异常(比如文件路径不可访问、文件不存在且无法创建等原因导致),打印异常的堆栈信息(用于调试排查问题),然后返回null,告知调用者获取打印流失败。 e.printStackTrace(); return null; } catch (NullPointerException e) { // 如果在创建文件输出流或者打印流过程中出现空指针异常(比如传入的文件对象为null等原因导致),打印异常的堆栈信息(用于调试排查问题),然后返回null,告知调用者获取打印流失败。 e.printStackTrace(); return null; } // 如果成功创建了打印流对象ps,返回该对象,供调用者(如exportToText方法)使用该打印流将数据输出到文件中进行文本导出操作。 return ps; } /** * 生成挂载在SD卡上用于存储导入数据的文本文件,会尝试创建文件所在的目录以及文件本身,如果出现安全、IO等异常情况则返回null。 */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { // 创建一个可变字符串对象sb,用于拼接构建文件的完整路径。 StringBuilder sb = new StringBuilder(); // 将外部存储的根目录(通过Environment.getExternalStorageDirectory方法获取,通常是SD卡的根目录或者安卓设备上的外部存储根目录)添加到sb中,作为文件路径的起始部分。 sb.append(Environment.getExternalStorageDirectory()); // 通过context.getString方法获取文件路径相关的字符串资源(filePathResId对应的字符串),并添加到sb中,进一步构建文件的完整路径。 sb.append(context.getString(filePathResId)); // 使用拼接好的路径字符串创建一个File对象filedir,用于表示文件所在的目录,如果目录不存在后续会尝试创建它。 File filedir = new File(sb.toString()); // 继续向sb中添加内容,通过context.getString方法获取文件名格式相关的字符串资源(fileNameFormatResId对应的字符串),并使用DateFormat.format方法按照预定义的日期格式(context.getString(R.string.format_date_ymd)获取的格式字符串)对当前系统时间(System.currentTimeMillis获取当前时间戳)进行格式化,将格式化后的日期字符串作为文件名的一部分添加到sb中,构建完整的文件名及路径。 sb.append(context.getString( fileNameFormatResId, DateFormat.format(context.getString(R.string.format_date_ymd), System.currentTimeMillis()))); // 使用拼接好的完整路径字符串创建一个File对象file,用于表示最终要创建的文件,如果文件不存在后续会尝试创建它。 File file = new File(sb.toString()); try { // 判断文件所在的目录filedir是否不存在,如果不存在,调用mkdir方法尝试创建该目录,创建成功后该目录就可用于存放文件了。 if (!filedir.exists()) { filedir.mkdir(); } // 判断要创建的文件file是否不存在,如果不存在,调用createNewFile方法尝试创建该文件,创建成功后就可以用于存储数据了。 if (!file.exists()) { file.createNewFile(); } // 如果文件和目录创建成功,返回创建好的文件对象file,供调用者(如getExportToTextPrintStream方法)使用该文件进行后续操作(如创建输出流、写入数据等)。 return file; } catch (SecurityException e) { // 如果在创建目录或者文件过程中出现安全异常(比如没有足够的权限访问、创建文件或目录等原因导致),打印异常的堆栈信息(用于调试排查问题),然后返回null,告知调用者文件创建失败。 e.printStackTrace(); } catch (IOException e) { // 如果在创建目录或者文件过程中出现IO异常(比如磁盘空间不足、文件系统错误等原因导致读写文件出现问题),打印异常的堆栈信息(用于调试排查问题),然后返回null,告知调用者文件创建失败。 e.printStackTrace(); } // 如果出现上述异常情况导致文件或目录创建失败,返回null,告知调用者文件创建失败。 return null; }