diff --git a/NUNG95)KSCVJKJJAANMGS(8.png b/NUNG95)KSCVJKJJAANMGS(8.png new file mode 100644 index 0000000..78adfc7 Binary files /dev/null and b/NUNG95)KSCVJKJJAANMGS(8.png differ diff --git a/W3~]OT3)~ZILZ1W`2_1M`R4.png b/W3~]OT3)~ZILZ1W`2_1M`R4.png new file mode 100644 index 0000000..5d75240 Binary files /dev/null and b/W3~]OT3)~ZILZ1W`2_1M`R4.png differ diff --git a/doc/字符统计/物理体系结构图.png b/doc/字符统计/物理体系结构图.png new file mode 100644 index 0000000..2cf1bc7 Binary files /dev/null and b/doc/字符统计/物理体系结构图.png differ diff --git a/doc/显示实时天气/物理体系结构体.png b/doc/显示实时天气/物理体系结构体.png new file mode 100644 index 0000000..8d353ab Binary files /dev/null and b/doc/显示实时天气/物理体系结构体.png differ diff --git a/doc/泛读报告.docx b/doc/泛读报告.docx index 39bb0ad..016d375 100644 Binary files a/doc/泛读报告.docx and b/doc/泛读报告.docx differ diff --git a/doc/精读报告/tool/BackupUtils.doc b/doc/精读报告/tool/BackupUtils.doc new file mode 100644 index 0000000..5abf325 --- /dev/null +++ b/doc/精读报告/tool/BackupUtils.doc @@ -0,0 +1,345 @@ +/* + * 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;// 定义一个常量,表示当前的状态是 SD 卡没有挂载 + // 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;// 定义一个私有变量,用于保存一个 TextExport 对象的引用 + + private BackupUtils(Context context) { + // 定义一个私有构造器,用于创建 BackupUtils 对象mTextExport = new TextExport(context);// 用传入的上下文参数创建一个 TextExport 对象,并赋值给 mTextExport 变量 + } + + private static boolean externalStorageAvailable() {// 定义一个私有静态方法,用于判断外部存储是否可用 + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + public int exportToText() { + // 定义一个公有方法,用于导出文本文件 + return mTextExport.exportToText();//调用 mTextExport 对象的 exportToText 方法,并返回其结果 + } + + public String getExportedTextFileName() {// 定义一个公有方法,用于获取导出的文本文件名 + return mTextExport.mFileName;// 返回 mTextExport 对象的 mFileName 变量的值 + } + + public String getExportedTextFileDir() {// 定义一个公有方法,用于获取导出的文本文件目录 + return mTextExport.mFileDirectory;// 返回 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;// 定义一个私有静态常量,表示笔记 ID 列在 NOTE_PROJECTION 数组中的索引 + + private static final int NOTE_COLUMN_MODIFIED_DATE = 1;// 定义一个私有静态常量,表示笔记修改日期列在 NOTE_PROJECTION 数组中的索引 + + private static final int NOTE_COLUMN_SNIPPET = 2; // 定义一个私有静态常量,表示笔记摘要列在 NOTE_PROJECTION 数组中的索引 + + 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;// 定义一个私有静态常量,表示数据内容列在 DATA_PROJECTION 数组中的索引 + + private static final int DATA_COLUMN_MIME_TYPE = 1;// 定义一个私有静态常量,表示数据 MIME 类型列在 DATA_PROJECTION 数组中的索引 + + private static final int DATA_COLUMN_CALL_DATE = 2;// 定义一个私有静态常量,表示数据 MIME 类型列在 DATA_PROJECTION 数组中的索引 + + private static final int DATA_COLUMN_PHONE_NUMBER = 4;// 定义一个私有静态常量,表示数据 3 列在 DATA_PROJECTION 数组中的索引,用于存储电话号码 + + private final String [] TEXT_FORMAT;// 定义一个私有不可变数组,用于存储文本文件的格式字符串 + private static final int FORMAT_FOLDER_NAME = 0;// 定义一个私有静态常量,表示文件夹名称格式在 TEXT_FORMAT 数组中的索引 + private static final int FORMAT_NOTE_DATE = 1;// 定义一个私有静态常量,表示笔记日期格式在 TEXT_FORMAT 数组中的索引 + private static final int FORMAT_NOTE_CONTENT = 2;// 定义一个私有静态常量,表示笔记内容格式在 TEXT_FORMAT 数组中的索引 + + private Context mContext;// 定义一个私有变量,用于保存上下文对象的引用 + private String mFileName;// 定义一个私有变量,用于保存导出的文本文件名 + private String mFileDirectory;// 定义一个私有变量,用于保存导出的文本文件目录 + + public TextExport(Context context) {// 定义一个公有构造器,用于创建 TextExport 对象 + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);// 用传入的上下文参数获取资源数组,并赋值给 TEXT_FORMAT 数组 + mContext = context; // 用传入的上下文参数赋值给 mContext 变量 + mFileName = "";// 初始化 mFileName 变量为空字符串 + mFileDirectory = "";// 初始化 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) {// 定义一个私有方法,用于导出指定文件夹 ID 的文本文件,需要传入文件夹 ID 和打印流对象作为参数 + // Query notes belong to this folder + Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,// 用 mContext 变量获取内容解析器,并查询笔记表的 URI + NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {// 指定需要返回的列名数组为 NOTE_PROJECTION + 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);// 用游标获取当前行的笔记 ID 列的值,转换为字符串,并赋值给 noteId 变量 + exportNoteToText(noteId, ps);// 调用 exportNoteToText 方法,传入笔记 ID 和打印流对象作为参数,导出该笔记的文本文件 + } while (notesCursor.moveToNext());// 循环条件为游标移动到下一行,直到没有更多行为止 + } + notesCursor.close();//关闭游标对象,释放资源 + } + } + + /** + * Export note identified by id to a print stream + */ + private void exportNoteToText(String noteId, PrintStream ps) { // 定义一个私有方法,用于导出指定笔记 ID 的文本文件,需要传入笔记 ID 和打印流对象作为参数 + Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,// 用 mContext 变量获取内容解析器,并查询数据表的 URI + DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] {// 指定查询条件为笔记 ID 等于传入的笔记 ID noteId }, null); // 指定排序方式为 null + 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(); // 关闭数据游标 + } + // 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()); // 打印错误日志 + } + } + + /** + * Note will be exported as text which is user readable + */ + public int exportToText() { // 定义一个导出文本的方法 + if (!externalStorageAvailable()) { // 如果外部存储不可用 + Log.d(TAG, "Media was not mounted"); // 打印调试日志 + return STATE_SD_CARD_UNMOUONTED; // 返回SD卡未挂载的状态 + } + + PrintStream ps = getExportToTextPrintStream(); // 获取导出文本的打印流 + if (ps == null) { // 如果打印流为空 + Log.e(TAG, "get print stream error"); // 打印错误日志 + return STATE_SYSTEM_ERROR; // 返回系统错误的状态 + } + // 首先导出文件夹和它们的便签 + 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); // 查询文件夹类型的便签,排除回收站和通话记录文件夹 + + 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); // 获取文件夹的ID + exportFolderToText(folderId, ps); // 调用导出文件夹到文本的方法,传入文件夹ID和打印流 + } while (folderCursor.moveToNext()); // 循环直到文件夹游标移动到最后一行 + } + folderCursor.close(); // 关闭文件夹游标 + } + + // Export notes in root's folder + 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); // 获取便签的ID + exportNoteToText(noteId, ps); // 调用导出便签到文本的方法,传入便签ID和打印流 + } while (noteCursor.moveToNext()); // 循环直到便签游标移动到最后一行 + } + noteCursor.close(); // 关闭便签游标 + } + ps.close(); // 关闭打印流 + + return STATE_SUCCESS; // 返回成功的状态 + } + + /** + * Get a print stream pointed to the file {@generateExportedTextFile} + */ + private PrintStream getExportToTextPrintStream() { // 定义一个获取导出文本的打印流的方法 + File file = generateFileMountedOnSDcard(mContext, R.string.file_path, + R.string.file_name_txt_format); // 调用生成挂载在SD卡上的文件的方法,传入上下文,文件路径和文件名格式 + 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; // 返回打印流 + } + } + + /** + * Generate the text file to store imported data + */ + private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { // 定义一个生成挂载在SD卡上的文件的方法,传入上下文,文件路径资源ID和文件名格式资源ID + StringBuilder sb = new StringBuilder(); // 创建一个字符串构建器 + sb.append(Environment.getExternalStorageDirectory()); // 追加外部存储目录 + sb.append(context.getString(filePathResId)); // 追加文件路径字符串 + File filedir = new File(sb.toString()); // 创建一个文件目录对象,传入字符串构建器的内容 + 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) { // 如果发生安全异常 + e.printStackTrace(); // 打印异常堆栈 + } catch (IOException e) { // 如果发生输入输出异常 + e.printStackTrace(); // 打印异常堆栈 + } + + return null; // 返回空值 + } +} + + diff --git a/doc/精读报告/tool/DataUtils.doc b/doc/精读报告/tool/DataUtils.doc new file mode 100644 index 0000000..472fd39 --- /dev/null +++ b/doc/精读报告/tool/DataUtils.doc @@ -0,0 +1,306 @@ +/* + * 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.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + +import java.util.ArrayList; +import java.util.HashSet; + + +public class DataUtils { // 定义一个数据工具类 + public static final String TAG = "DataUtils"; // 定义一个静态常量字符串,表示日志标签 + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { // 定义一个静态方法,批量删除便签,传入内容解析器和便签ID的集合 + if (ids == null) { // 如果ID集合为空 + Log.d(TAG, "the ids is null"); // 打印调试日志 + return true; // 返回真值 + } + if (ids.size() == 0) { // 如果ID集合的大小为零 + Log.d(TAG, "no id is in the hashset"); // 打印调试日志 + return true; // 返回真值 + } + + ArrayList operationList = new ArrayList(); // 创建一个内容提供者操作的列表 + for (long id : ids) { // 遍历ID集合中的每个ID + if(id == Notes.ID_ROOT_FOLDER) { // 如果ID是根文件夹的ID + Log.e(TAG, "Don't delete system folder root"); // 打印错误日志 + continue; // 跳过本次循环 + } + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); // 创建一个内容提供者操作的构建器,指定删除便签的URI和ID + operationList.add(builder.build()); // 把构建器构建出来的操作添加到操作列表中 + } + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); // 调用内容解析器的批量应用方法,传入便签的授权和操作列表,获取结果数组 + if (results == null || results.length == 0 || results[0] == null) { // 如果结果数组为空或长度为零或第一个元素为空 + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); // 打印调试日志,显示失败的ID集合 + return false; // 返回假值 + } + return true; // 返回真值 + } catch (RemoteException e) { // 如果发生远程异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 打印错误日志,显示异常信息 + } catch (OperationApplicationException e) { // 如果发生操作应用异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 打印错误日志,显示异常信息 + } + return false; // 返回假值 + } + + public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { // 定义一个静态方法,移动便签到文件夹,传入内容解析器,便签ID,源文件夹ID和目标文件夹ID + ContentValues values = new ContentValues(); // 创建一个内容值对象 + values.put(NoteColumns.PARENT_ID, desFolderId); // 把目标文件夹ID作为父ID放入内容值中 + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 把源文件夹ID作为原始父ID放入内容值中 + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 把本地修改标志设为1放入内容值中 + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); // 调用内容解析器的更新方法,传入便签的URI和ID,内容值,空的选择和选择参数 + } + + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, + long folderId) { // 定义一个静态方法,批量移动到文件夹,传入内容解析器,便签ID的集合和文件夹ID + if (ids == null) { // 如果ID集合为空 + Log.d(TAG, "the ids is null"); // 打印调试日志 + return true; // 返回真值 + } + + ArrayList operationList = new ArrayList(); // 创建一个内容提供者操作的列表 + for (long id : ids) { // 遍历ID集合中的每个ID + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); // 创建一个内容提供者操作的构建器,指定更新便签的URI和ID + builder.withValue(NoteColumns.PARENT_ID, folderId); // 把文件夹ID作为父ID放入构建器中 + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 把本地修改标志设为1放入构建器中 + operationList.add(builder.build()); // 把构建器构建出来的操作添加到操作列表中 + } + + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); // 调用内容解析器的批量应用方法,传入便签的授权和操作列表,获取结果数组 + if (results == null || results.length == 0 || results[0] == null) { // 如果结果数组为空或长度为零或第一个元素为空 + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); // 打印调试日志,显示失败的ID集合 + return false; // 返回假值 + } + return true; // 返回真值 + } catch (RemoteException e) { // 如果发生远程异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 打印错误日志,显示异常信息 + } catch (OperationApplicationException e) { // 如果发生操作应用异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 打印错误日志,显示异常信息 + } + return false; // 返回假值 + } + + /** + * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + */ + public static int getUserFolderCount(ContentResolver resolver) { // 定义一个静态方法,获取用户文件夹的数量,传入内容解析器 + Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { "COUNT(*)" }, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, + null); // 查询便签的URI,返回文件夹类型且不在回收站的便签的数量 + + int count = 0; // 声明一个整型变量,表示数量 + if(cursor != null) { // 如果游标不为空 + if(cursor.moveToFirst()) { // 如果游标移动到第一行 + try { + count = cursor.getInt(0); // 获取游标的第一列的值,即数量 + } catch (IndexOutOfBoundsException e) { // 如果发生索引越界异常 + Log.e(TAG, "get folder count failed:" + e.toString()); // 打印错误日志,显示异常信息 + } finally { + cursor.close(); // 最终关闭游标 + } + } + } + return count; // 返回数量 + } + + public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { // 定义一个静态方法,判断便签是否在数据库中可见,传入内容解析器,便签ID和类型 + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, + new String [] {String.valueOf(type)}, + null); // 查询便签的URI和ID,返回指定类型且不在回收站的便签 + + boolean exist = false; // 声明一个布尔型变量,表示是否存在 + if (cursor != null) { // 如果游标不为空 + if (cursor.getCount() > 0) { // 如果游标的数量大于零 + exist = true; // 把存在设为真值 + } + cursor.close(); // 关闭游标 + } + return exist; // 返回是否存在 + } + + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + // 查询便签的URI和ID,返回便签的所有列 + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, null, null, null); + + boolean exist = false; // 声明一个布尔型变量,表示是否存在 + if (cursor != null) { // 如果游标不为空 + if (cursor.getCount() > 0) { // 如果游标的数量大于零 + exist = true; // 把存在设为真值 + } + cursor.close(); // 关闭游标 + } + return exist; // 返回是否存在 + } + + + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + // 查询数据的URI和ID,返回数据的所有列 + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), + null, null, null, null); + + boolean exist = false; // 声明一个布尔型变量,表示是否存在 + if (cursor != null) { // 如果游标不为空 + if (cursor.getCount() > 0) { // 如果游标的数量大于零 + exist = true; // 把存在设为真值 + } + cursor.close(); // 关闭游标 + } + return exist; // 返回是否存在 + } + + public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { + // 查询便签的URI,返回文件夹类型且不在回收站且名称等于指定值的便签 + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null, + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.SNIPPET + "=?", + new String[] { name }, null); + + boolean exist = false; // 声明一个布尔型变量,表示是否存在 + if(cursor != null) { // 如果游标不为空 + if(cursor.getCount() > 0) { // 如果游标的数量大于零 + exist = true; // 把存在设为真值 + } + cursor.close(); // 关闭游标 + } + return exist; // 返回是否存在 + } + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + // 查询便签的URI,返回指定父ID的便签的小部件ID和类型 + Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, + NoteColumns.PARENT_ID + "=?", + new String[] { String.valueOf(folderId) }, + null); + + HashSet set = null; // 声明一个哈希集合,用于存储便签小部件的属性 + if (c != null) { // 如果游标不为空 + if (c.moveToFirst()) { // 如果游标移动到第一行 + set = new HashSet(); // 创建一个新的哈希集合 + do { + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); // 创建一个新的便签小部件属性对象 + widget.widgetId = c.getInt(0); // 获取游标的第一列的值,即小部件ID + widget.widgetType = c.getInt(1); // 获取游标的第二列的值,即小部件类型 + set.add(widget); // 把便签小部件属性对象添加到哈希集合中 + } catch (IndexOutOfBoundsException e) { // 如果发生索引越界异常 + Log.e(TAG, e.toString()); // 打印错误日志,显示异常信息 + } + } while (c.moveToNext()); // 当游标移动到下一行时,重复上述操作 + } + c.close(); // 关闭游标 + } + return set; // 返回哈希集合 + } + + + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + // 查询数据的URI,返回指定便签ID和MIME类型的数据的电话号码 + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.PHONE_NUMBER }, + CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", + new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE }, + null); + + if (cursor != null && cursor.moveToFirst()) { // 如果游标不为空且移动到第一行 + try { + return cursor.getString(0); // 返回游标的第一列的值,即电话号码 + } catch (IndexOutOfBoundsException e) { // 如果发生索引越界异常 + Log.e(TAG, "Get call number fails " + e.toString()); // 打印错误日志,显示异常信息 + } finally { + cursor.close(); // 最终关闭游标 + } + } + return ""; // 返回空字符串 + } + + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + // 查询数据的URI,返回指定通话日期、MIME类型和电话号码的数据的便签ID + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.NOTE_ID }, + CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" + + CallNote.PHONE_NUMBER + ",?)", + new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, + null); + + if (cursor != null) { // 如果游标不为空 + if (cursor.moveToFirst()) { // 如果游标移动到第一行 + try { + return cursor.getLong(0); // 返回游标的第一列的值,即便签ID + } catch (IndexOutOfBoundsException e) { // 如果发生索引越界异常 + Log.e(TAG, "Get call note id fails " + e.toString()); // 打印错误日志,显示异常信息 + } + } + cursor.close(); // 关闭游标 + } + return 0; // 返回0 + } + + + public static String getSnippetById(ContentResolver resolver, long noteId) { + // 查询便签的URI,返回指定ID的便签的摘要 + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String [] { NoteColumns.SNIPPET }, + NoteColumns.ID + "=?", + new String [] { String.valueOf(noteId)}, + null); + + if (cursor != null) { // 如果游标不为空 + String snippet = ""; // 声明一个字符串变量,表示摘要 + if (cursor.moveToFirst()) { // 如果游标移动到第一行 + snippet = cursor.getString(0); // 获取游标的第一列的值,即摘要 + } + cursor.close(); // 关闭游标 + return snippet; // 返回摘要 + } + throw new IllegalArgumentException("Note is not found with id: " + noteId); // 抛出非法参数异常,显示错误信息 + } + + + public static String getFormattedSnippet(String snippet) { + if (snippet != null) { // 如果摘要不为空 + snippet = snippet.trim(); // 去掉摘要两端的空格 + int index = snippet.indexOf('\n'); // 查找摘要中第一个换行符的位置 + if (index != -1) { // 如果找到了换行符 + snippet = snippet.substring(0, index); // 截取换行符之前的部分作为新的摘要 + } + } + return snippet; // 返回格式化后的摘要字符串 + } +} diff --git a/doc/精读报告/tool/GTaskStringUtils.doc b/doc/精读报告/tool/GTaskStringUtils.doc new file mode 100644 index 0000000..a147377 --- /dev/null +++ b/doc/精读报告/tool/GTaskStringUtils.doc @@ -0,0 +1,115 @@ +/* + * 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; + +public class GTaskStringUtils { + + // 定义一些常量字符串,表示Google任务的JSON格式的属性名 + + public final static String GTASK_JSON_ACTION_ID = "action_id"; // 动作ID + + public final static String GTASK_JSON_ACTION_LIST = "action_list"; // 动作列表 + + public final static String GTASK_JSON_ACTION_TYPE = "action_type"; // 动作类型 + + public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; // 创建动作 + + public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; // 获取所有动作 + + public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; // 移动动作 + + public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; // 更新动作 + + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; // 创建者ID + + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; // 子实体 + + public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; // 客户端版本 + + public final static String GTASK_JSON_COMPLETED = "completed"; // 完成状态 + + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; // 当前列表ID + + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; // 默认列表ID + + public final static String GTASK_JSON_DELETED = "deleted"; // 删除状态 + + public final static String GTASK_JSON_DEST_LIST = "dest_list"; // 目标列表 + + public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; // 目标父实体 + + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; // 目标父实体类型 + + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; // 实体变化 + + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型 + + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; // 获取删除状态 + + public final static String GTASK_JSON_ID = "id"; // ID + + public final static String GTASK_JSON_INDEX = "index"; // 索引 + + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间 + + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; // 最新同步点 + + public final static String GTASK_JSON_LIST_ID = "list_id"; // 列表ID + + public final static String GTASK_JSON_LISTS = "lists"; // 列表 + + public final static String GTASK_JSON_NAME = "name"; // 名称 + + public final static String GTASK_JSON_NEW_ID = "new_id"; // 新ID + + public final static String GTASK_JSON_NOTES = "notes"; // 便签 + + public final static String GTASK_JSON_PARENT_ID = "parent_id"; // 父ID + + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; // 前一个兄弟ID + + public final static String GTASK_JSON_RESULTS = "results"; // 结果 + + public final static String GTASK_JSON_SOURCE_LIST = "source_list"; // 源列表 + + public final static String GTASK_JSON_TASKS = "tasks"; // 任务 + + public final static String GTASK_JSON_TYPE = "type"; // 类型 + + public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; // 组类型 + + public final static String GTASK_JSON_TYPE_TASK = "TASK"; // 任务类型 + + public final static String GTASK_JSON_USER = "user"; // 用户 + + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; // MIUI文件夹的前缀 + + public final static String FOLDER_DEFAULT = "Default"; // 默认文件夹的名称 + + public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话便签文件夹的名称 + + public final static String FOLDER_META = "METADATA"; // 元数据文件夹的名称 + + public final static String META_HEAD_GTASK_ID = "meta_gid"; // 元数据中的Google任务ID的头部 + + public final static String META_HEAD_NOTE = "meta_note"; // 元数据中的便签的头部 + + public final static String META_HEAD_DATA = "meta_data"; // 元数据中的数据的头部 + + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; // 元数据便签的名称,提示不要更新和删除 + +} diff --git a/doc/精读报告/tool/ResourceParser.doc b/doc/精读报告/tool/ResourceParser.doc new file mode 100644 index 0000000..2708d61 --- /dev/null +++ b/doc/精读报告/tool/ResourceParser.doc @@ -0,0 +1,214 @@ +/* + * 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.preference.PreferenceManager; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +public class ResourceParser { + + // 定义一些常量整型,表示便签的背景颜色 + public static final int YELLOW = 0; + public static final int BLUE = 1; + public static final int WHITE = 2; + public static final int GREEN = 3; + public static final int RED = 4; + + // 定义一个常量整型,表示默认的背景颜色 + public static final int BG_DEFAULT_COLOR = YELLOW; + + // 定义一些常量整型,表示便签的文字大小 + public static final int TEXT_SMALL = 0; + public static final int TEXT_MEDIUM = 1; + public static final int TEXT_LARGE = 2; + public static final int TEXT_SUPER = 3; + + // 定义一个常量整型,表示默认的文字大小 + public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + + // 定义一个静态内部类,用于存储便签的背景资源 + public static class NoteBgResources { + // 定义一个静态整型数组,表示编辑模式下的便签背景资源 + private final static int [] BG_EDIT_RESOURCES = new int [] { + R.drawable.edit_yellow, + R.drawable.edit_blue, + R.drawable.edit_white, + R.drawable.edit_green, + R.drawable.edit_red + }; + + // 定义一个静态整型数组,表示编辑模式下的便签标题背景资源 + private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { + R.drawable.edit_title_yellow, + R.drawable.edit_title_blue, + R.drawable.edit_title_white, + R.drawable.edit_title_green, + R.drawable.edit_title_red + }; + + // 定义一个静态方法,根据ID获取便签背景资源 + public static int getNoteBgResource(int id) { + return BG_EDIT_RESOURCES[id]; + } + + // 定义一个静态方法,根据ID获取便签标题背景资源 + public static int getNoteTitleBgResource(int id) { + return BG_EDIT_TITLE_RESOURCES[id]; + } + } + + // 定义一个静态方法,获取默认的背景ID + public static int getDefaultBgId(Context context) { + // 如果用户设置了随机背景颜色 + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + // 返回一个随机的背景ID + return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); + } else { + // 否则返回默认的背景ID + return BG_DEFAULT_COLOR; + } + } + + // 这是一个公共静态类,用于存储笔记项的背景资源 + public static class NoteItemBgResources { + // 这是一个整型数组,用于存储第一个笔记项的背景资源 + private final static int [] BG_FIRST_RESOURCES = new int [] { + R.drawable.list_yellow_up, + R.drawable.list_blue_up, + R.drawable.list_white_up, + R.drawable.list_green_up, + R.drawable.list_red_up + }; + + // 这是一个整型数组,用于存储中间笔记项的背景资源 + private final static int [] BG_NORMAL_RESOURCES = new int [] { + R.drawable.list_yellow_middle, + R.drawable.list_blue_middle, + R.drawable.list_white_middle, + R.drawable.list_green_middle, + R.drawable.list_red_middle + }; + + // 这是一个整型数组,用于存储最后一个笔记项的背景资源 + private final static int [] BG_LAST_RESOURCES = new int [] { + R.drawable.list_yellow_down, + R.drawable.list_blue_down, + R.drawable.list_white_down, + R.drawable.list_green_down, + R.drawable.list_red_down, + }; + + // 这是一个整型数组,用于存储单个笔记项的背景资源 + private final static int [] BG_SINGLE_RESOURCES = new int [] { + R.drawable.list_yellow_single, + R.drawable.list_blue_single, + R.drawable.list_white_single, + R.drawable.list_green_single, + R.drawable.list_red_single + }; + + // 这是一个公共静态方法,用于根据id返回第一个笔记项的背景资源 + public static int getNoteBgFirstRes(int id) { + return BG_FIRST_RESOURCES[id]; + } + + // 这是一个公共静态方法,用于根据id返回最后一个笔记项的背景资源 + public static int getNoteBgLastRes(int id) { + return BG_LAST_RESOURCES[id]; + } + + // 这是一个公共静态方法,用于根据id返回单个笔记项的背景资源 + public static int getNoteBgSingleRes(int id) { + return BG_SINGLE_RESOURCES[id]; + } + + // 这是一个公共静态方法,用于根据id返回中间笔记项的背景资源 + public static int getNoteBgNormalRes(int id) { + return BG_NORMAL_RESOURCES[id]; + } + + // 这是一个公共静态方法,用于返回文件夹的背景资源 + // 这是一个公共静态方法,用于返回文件夹的背景资源 + public static int getFolderBgRes() { + return R.drawable.list_folder; + } + } + + // 这是一个公共静态类,用于存储小部件的背景资源 + public static class WidgetBgResources { + // 这是一个整型数组,用于存储2x2小部件的背景资源 + private final static int [] BG_2X_RESOURCES = new int [] { + R.drawable.widget_2x_yellow, + R.drawable.widget_2x_blue, + R.drawable.widget_2x_white, + R.drawable.widget_2x_green, + R.drawable.widget_2x_red, + }; + + // 这是一个公共静态方法,用于根据id返回2x2小部件的背景资源 + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + // 这是一个整型数组,用于存储4x4小部件的背景资源 + private final static int [] BG_4X_RESOURCES = new int [] { + R.drawable.widget_4x_yellow, + R.drawable.widget_4x_blue, + R.drawable.widget_4x_white, + R.drawable.widget_4x_green, + R.drawable.widget_4x_red + }; + + // 这是一个公共静态方法,用于根据id返回4x4小部件的背景资源 + public static int getWidget4xBgResource(int id) { + return BG_4X_RESOURCES[id]; + } + } + + // 这是一个公共静态类,用于存储文本外观的资源 + public static class TextAppearanceResources { + // 这是一个整型数组,用于存储不同大小的文本外观资源 + private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { + R.style.TextAppearanceNormal, + R.style.TextAppearanceMedium, + R.style.TextAppearanceLarge, + R.style.TextAppearanceSuper + }; + + // 这是一个公共静态方法,用于根据id返回文本外观资源 + public static int getTexAppearanceResource(int id) { + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; + } + return TEXTAPPEARANCE_RESOURCES[id]; + } + + // 这是一个公共静态方法,用于返回文本外观资源的大小 + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +} diff --git a/doc/精读报告/ui/NotesListActivity.doc b/doc/精读报告/ui/NotesListActivity.doc new file mode 100644 index 0000000..784995a --- /dev/null +++ b/doc/精读报告/ui/NotesListActivity.doc @@ -0,0 +1,1220 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +// 定义一个类,继承自Activity类,实现OnClickListener和OnItemLongClickListener接口 +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 定义一个常量,表示文件夹笔记列表查询的标识符 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + + // 定义一个常量,表示文件夹列表查询的标识符 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + // 定义一个常量,表示菜单中删除文件夹的选项 + private static final int MENU_FOLDER_DELETE = 0; + + // 定义一个常量,表示菜单中查看文件夹的选项 + private static final int MENU_FOLDER_VIEW = 1; + + // 定义一个常量,表示菜单中修改文件夹名称的选项 + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + // 定义一个常量,表示偏好设置中添加介绍的选项 + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + // 定义一个枚举类型,表示列表编辑的状态,有三种可能:笔记列表、子文件夹、通话记录文件夹 + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + + // 定义一个变量,表示当前的列表编辑状态 + private ListEditState mState; + + // 定义一个变量,表示后台查询处理器 + private BackgroundQueryHandler mBackgroundQueryHandler; + + // 定义一个变量,表示笔记列表适配器 + private NotesListAdapter mNotesListAdapter; + + // 定义一个变量,表示笔记列表视图 + private ListView mNotesListView; + + private Button mAddNewNote; // 添加新便签的按钮 + private boolean mDispatch; // 用于判断是否需要分发触摸事件 + private int mOriginY; // 用于记录触摸事件的原始 Y 坐标 + private int mDispatchY; // 用于记录分发触摸事件的 Y 坐标 + private TextView mTitleBar; // 用于显示标题栏的文本视图 + private long mCurrentFolderId; // 用于记录当前文件夹的 ID + private ContentResolver mContentResolver; // 用于访问内容提供器的对象 + private ModeCallback mModeCallBack; // 用于处理上下文菜单的回调对象 + private static final String TAG = "NotesListActivity"; // 定义一个常量字符串,用于日志输出 + + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; // 定义一个常量整数,用于设置便签列表视图的滚动速率 + + private NoteItemData mFocusNoteDataItem; // 用于记录当前焦点便签的数据对象 + + // 定义一些常量字符串,用于查询便签数据库的条件 + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + + " AND " + NoteColumns.NOTES_COUNT + ">0)"; + + // 定义一些常量整数,用于标识不同的请求码 + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + + @Override + protected void onCreate(Bundle savedInstanceState) {// 调用父类的方法 + super.onCreate(savedInstanceState);// 设置布局文件为note_list.xml + + setContentView(R.layout.note_list);// 初始化资源 + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); // 从原始资源文件中设置应用程序的介绍信息 + } + + @Override + // 这个方法是在一个Activity从另一个Activity返回结果时调用的 + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 如果结果是成功的,并且请求码是打开或新建一个节点,那么就更新列表适配器的数据源 + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + // 否则,调用父类的方法处理结果 + super.onActivityResult(requestCode, resultCode, data); + } + } + + // 这个方法是从raw资源文件中读取应用信息,并保存到SharedPreferences中 + private void setAppInfoFromRawRes() { + // 获取SharedPreferences对象 + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 如果没有添加过介绍信息,就执行以下操作 + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + // 创建一个StringBuilder对象,用来存储读取的内容 + StringBuilder sb = new StringBuilder(); + // 声明一个输入流对象 + InputStream in = null; + try { + // 从raw资源文件中打开输入流 + in = getResources().openRawResource(R.raw.introduction); + // 如果输入流不为空,就创建一个字符输入流和缓冲输入流,按照字符数组读取内容,并追加到StringBuilder中 + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char [] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + // 如果输入流为空,就打印错误日志并返回 + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + // 如果发生异常,就打印堆栈信息并返回 + e.printStackTrace(); + return; + } finally { + // 最后,如果输入流不为空,就关闭输入流 + if(in != null) { + try { + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + // 这个代码段是在读取完介绍信息后,创建一个空的WorkingNote对象,并设置其属性和内容 + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + // 设置WorkingNote对象的文本内容为StringBuilder中的内容 + note.setWorkingText(sb.toString()); + // 如果保存WorkingNote对象成功,就把SharedPreferences中的添加介绍信息的标志设为true + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + // 如果保存失败,就打印错误日志并返回 + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + // 这个方法是在Activity启动时调用的 + protected void onStart() { + // 调用父类的方法 + super.onStart(); + // 开始异步查询笔记列表 + startAsyncNotesListQuery(); + } + + // 这个方法是初始化资源的 + private void initResources() { + // 获取内容解析器对象 + mContentResolver = this.getContentResolver(); + // 创建一个后台查询处理器对象 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + // 设置当前文件夹的ID为根文件夹的ID + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 获取笔记列表视图对象,并设置其底部视图和点击监听器 + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + // 创建一个笔记列表适配器对象,并设置给笔记列表视图 + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + // 获取添加新笔记按钮对象,并设置其点击监听器和触摸监听器 + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + // 初始化一些变量,用来控制事件分发和标题栏显示 + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + // 设置当前的列表编辑状态为笔记列表状态 + mState = ListEditState.NOTE_LIST; + // 创建一个模式回调对象,用来处理上下文菜单的操作 + mModeCallBack = new ModeCallback(); + } + + // 这个内部类实现了多选模式监听器和菜单项点击监听器的接口,用来处理列表视图的多选模式和菜单操作 + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + // 声明一个下拉菜单对象,一个操作模式对象和一个移动菜单项对象 + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + // 这个方法是在创建操作模式时调用的 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 从资源文件中加载菜单项,并设置删除和移动菜单项的点击监听器 + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + // 如果当前的文件夹是通话记录文件夹或者用户没有自定义文件夹,就隐藏移动菜单项 + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + // 否则,显示移动菜单项,并设置其点击监听器 + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + // 保存操作模式对象的引用,并设置列表适配器的选择模式为true + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); + // 设置列表视图不可长按,并隐藏添加新笔记按钮 + mNotesListView.setLongClickable(false); + mAddNewNote.setVisibility(View.GONE); + + // 从布局文件中加载自定义视图,并设置给操作模式对象 + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + // 创建一个下拉菜单对象,并设置其按钮和菜单资源 + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + // 设置下拉菜单的菜单项点击监听器,用来实现全选或取消全选的功能,并更新菜单状态 + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + return true; + } + // 这个方法是更新菜单状态的 + private void updateMenu() { + // 获取已选择的笔记数量 + int selectedCount = mNotesListAdapter.getSelectedCount(); + // 更新下拉菜单的标题,显示已选择的数量 + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + // 获取下拉菜单中的全选菜单项,并根据是否全选设置其标题和状态 + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + // 这个方法是在准备操作模式时调用的,这里没有实现任何功能 + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + // 这个方法是在点击操作模式中的菜单项时调用的,这里没有实现任何功能 + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + // 这个方法是在销毁操作模式时调用的 + public void onDestroyActionMode(ActionMode mode) { + // 设置列表适配器的选择模式为false,并恢复列表视图的长按功能和添加新笔记按钮的可见性 + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + // 这个方法是结束操作模式的 + public void finishActionMode() { + mActionMode.finish(); + } + + // 这个方法是在列表视图中的某一项被选中或取消选中时调用的 + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + // 设置列表适配器中对应位置的笔记为选中或未选中,并更新菜单状态 + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + // 这个方法是实现菜单项点击监听器的接口,用来处理删除和移动菜单项的点击事件 + public boolean onMenuItemClick(MenuItem item) { + // 如果没有选择任何笔记,就弹出一个提示信息,并返回true + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + // 根据菜单项的ID,执行相应的操作 + switch (item.getItemId()) { + // 如果是删除菜单项,就弹出一个确认对话框,询问用户是否要删除所选的笔记 + case R.id.delete: + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + // 如果用户点击确定,就调用批量删除的方法 + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + // 如果用户点击取消,就什么都不做 + builder.setNegativeButton(android.R.string.cancel, null); + // 显示对话框 + builder.show(); + break; + // 如果是移动菜单项,就开始查询目标文件夹的数据 + case R.id.move: + startQueryDestinationFolders(); + break; + // 如果是其他菜单项,就返回false + default: + return false; + } + return true; + } + } + + // 这个内部类实现了触摸监听器的接口,用来处理添加新笔记按钮的触摸事件 + private class NewNoteOnTouchListener implements OnTouchListener { + + // 这个方法是在触摸按钮时调用的 + public boolean onTouch(View v, MotionEvent event) { + // 根据触摸事件的类型,执行相应的操作 + switch (event.getAction()) { + // 如果是按下事件,就执行以下操作 + case MotionEvent.ACTION_DOWN: { + // 获取屏幕的显示对象,并获取屏幕的高度 + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + // 获取添加新笔记按钮的高度,并计算出按钮的起始位置 + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + // 获取触摸事件在屏幕上的Y坐标,并加上按钮的起始位置 + int eventY = start + (int) event.getY(); + /** + * Minus TitleBar's height + */ + // 如果当前的列表编辑状态是子文件夹状态,就减去标题栏的高度 + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + + // 如果触摸事件在一个特定的区域内,就执行以下操作 + if (event.getY() < (event.getX() * (-0.12) + 94)) { + // 获取列表视图中最后一个可见的视图对象(除了底部视图) + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + // 如果视图对象不为空,并且它的底部位置大于按钮的起始位置,并且它的顶部位置小于按钮的结束位置,就执行以下操作 + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + // 保存触摸事件在按钮上的Y坐标和在屏幕上的Y坐标 + mOriginY = (int) event.getY(); + mDispatchY = eventY; + // 设置触摸事件在屏幕上的Y坐标为之前保存的值 + event.setLocation(event.getX(), mDispatchY); + // 设置分发标志为true,并把触摸事件分发给列表视图处理 + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + // 如果是移动事件,就执行以下操作 + case MotionEvent.ACTION_MOVE: { + // 如果分发标志为true,就执行以下操作 + if (mDispatch) { + // 更新触摸事件在屏幕上的Y坐标,并设置给触摸事件对象 + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + // 把触摸事件分发给列表视图处理,并返回true + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + // 如果是其他类型的事件,就执行以下操作 + default: { + // 如果分发标志为true,就执行以下操作 + if (mDispatch) { + // 设置触摸事件在屏幕上的Y坐标为之前保存的值,并把分发标志设为false + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + // 把触摸事件分发给列表视图处理,并返回true + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + // 如果没有分发触摸事件,就返回false + return false; + } + + }; + + // 这个方法是开始异步查询笔记列表的 + private void startAsyncNotesListQuery() { + // 根据当前文件夹的ID,设置查询条件 + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + // 调用后台查询处理器对象,开始查询笔记的数据,并指定查询标识符、投影、条件和排序方式 + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + + // 这个内部类继承了异步查询处理器类,用来处理异步查询的结果 + private final class BackgroundQueryHandler extends AsyncQueryHandler { + // 构造方法,调用父类的构造方法,传入内容解析器对象 + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + // 这个方法是在查询完成时调用的,传入查询标识符、对象和游标 + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // 根据查询标识符,执行相应的操作 + switch (token) { + // 如果是查询笔记列表的标识符,就把游标传给列表适配器 + case FOLDER_NOTE_LIST_QUERY_TOKEN: + mNotesListAdapter.changeCursor(cursor); + break; + // 如果是查询文件夹列表的标识符,就判断游标是否有效,如果有效,就显示文件夹列表菜单 + case FOLDER_LIST_QUERY_TOKEN: + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + // 如果无效,就打印错误日志 + Log.e(TAG, "Query folder failed"); + } + break; + // 如果是其他标识符,就什么都不做 + default: + return; + } + } + } + + // 这个方法是显示文件夹列表菜单的,传入一个游标对象 + private void showFolderListMenu(Cursor cursor) { + // 创建一个对话框构造器对象,并设置其标题 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + // 创建一个文件夹列表适配器对象,并设置给对话框构造器 + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + // 设置对话框的点击监听器,用来处理用户选择某个文件夹的事件 + public void onClick(DialogInterface dialog, int which) { + // 调用数据工具类的方法,把已选择的笔记批量移动到用户选择的文件夹中 + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + // 弹出一个提示信息,显示移动了多少笔记到哪个文件夹中 + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + // 结束操作模式 + mModeCallBack.finishActionMode(); + } + }); + // 显示对话框 + builder.show(); + } + + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + // 定义一个私有方法,用于批量删除笔记 + private void batchDelete() { + // 创建一个异步任务,传入空参数,返回一个AppWidgetAttribute的集合 + new AsyncTask>() { + // 在后台线程执行的方法,返回要更新的小部件的集合 + protected HashSet doInBackground(Void... unused) { + // 获取选中的小部件的集合 + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 判断是否是同步模式 + if (!isSyncMode()) { + // 如果不是同步模式,直接删除选中的笔记 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + // 如果删除失败,打印错误日志 + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果是同步模式,将选中的笔记移动到回收站文件夹中 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + // 如果移动失败,打印错误日志 + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + // 返回要更新的小部件的集合 + return widgets; + } + + @Override + // 在主线程执行的方法,传入要更新的小部件的集合 + protected void onPostExecute(HashSet widgets) { + // 判断小部件集合是否为空 + if (widgets != null) { + // 遍历小部件集合 + for (AppWidgetAttribute widget : widgets) { + // 判断小部件的id和类型是否有效 + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + // 更新小部件的显示内容 + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + // 结束操作模式 + mModeCallBack.finishActionMode(); + } + }.execute(); // 执行异步任务 + } + + // 这个方法是删除一个文件夹的,传入一个文件夹的ID + private void deleteFolder(long folderId) { + // 如果文件夹的ID是根文件夹的ID,就打印错误日志,并返回 + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + // 创建一个哈希集合对象,用来存储要删除的文件夹的ID + HashSet ids = new HashSet(); + ids.add(folderId); + // 调用数据工具类的方法,获取该文件夹关联的小部件属性集合 + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + // 判断是否是同步模式 + if (!isSyncMode()) { + // 如果不是同步模式,就直接批量删除该文件夹 + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // 如果是同步模式,就把该文件夹批量移动到回收站文件夹中 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + // 如果小部件属性集合不为空,就遍历每个小部件属性对象,并更新对应的小部件 + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + // 这个方法是打开一个笔记节点的,传入一个笔记项数据对象 + private void openNode(NoteItemData data) { + // 创建一个意图对象,并设置其动作为查看 + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + // 把笔记项数据对象的ID作为额外数据传给意图对象 + intent.putExtra(Intent.EXTRA_UID, data.getId()); + // 用该意图启动一个新的Activity,并指定请求码 + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + // 这个方法是打开一个文件夹的,传入一个笔记项数据对象 + private void openFolder(NoteItemData data) { + // 设置当前文件夹的ID为笔记项数据对象的ID,并开始异步查询笔记列表 + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + // 判断笔记项数据对象的ID是否是通话记录文件夹的ID,如果是,就设置当前的列表编辑状态为通话记录文件夹状态,并隐藏添加新笔记按钮 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + // 否则,设置当前的列表编辑状态为子文件夹状态 + mState = ListEditState.SUB_FOLDER; + } + // 判断笔记项数据对象的ID是否是通话记录文件夹的ID,如果是,就设置标题栏的文本为通话记录文件夹的名称 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + // 否则,设置标题栏的文本为笔记项数据对象的摘要 + mTitleBar.setText(data.getSnippet()); + } + // 设置标题栏为可见 + mTitleBar.setVisibility(View.VISIBLE); + } + + // 这个方法是实现点击监听器的接口,用来处理点击事件 + public void onClick(View v) { + // 根据被点击的视图的ID,执行相应的操作 + switch (v.getId()) { + // 如果是添加新笔记按钮,就调用创建新笔记的方法 + case R.id.btn_new_note: + createNewNote(); + break; + // 如果是其他视图,就什么都不做 + default: + break; + } + } + + // 这个方法是显示软键盘的 + private void showSoftInput() { + // 获取输入法管理器对象 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + // 强制显示软键盘 + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + // 这个方法是隐藏软键盘的,传入一个视图对象 + private void hideSoftInput(View view) { + // 获取输入法管理器对象 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 隐藏软键盘,并传入视图对象的窗口标识符 + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + // 这个方法是显示创建或修改文件夹的对话框的,传入一个布尔值,表示是否是创建 + private void showCreateOrModifyFolderDialog(final boolean create) { + // 创建一个对话框构造器对象 + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 从布局文件中加载一个视图对象,并获取其中的编辑文本对象 + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + // 显示软键盘 + showSoftInput(); + // 如果不是创建,就执行以下操作 + if (!create) { + // 如果当前的焦点笔记项数据对象不为空,就把它的摘要设置给编辑文本对象,并设置对话框的标题为修改文件夹名称 + if (mFocusNoteDataItem != null) { + etName.setText(mFocusNoteDataItem.getSnippet()); + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + // 如果为空,就打印错误日志,并返回 + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + // 如果是创建,就清空编辑文本对象,并设置对话框的标题为创建文件夹 + etName.setText(""); + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + // 设置对话框的确定按钮,但不设置点击监听器,因为要在后面自定义点击事件的逻辑 + builder.setPositiveButton(android.R.string.ok, null); + // 设置对话框的取消按钮,并设置点击监听器,用来隐藏软键盘 + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); + + // 创建并显示对话框,并获取其中的确定按钮对象 + final Dialog dialog = builder.setView(view).show(); + final Button positive = (Button)dialog.findViewById(android.R.id.button1); + // 设置确定按钮的点击监听器,用来处理用户输入的文件夹名称 + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + // 隐藏软键盘 + hideSoftInput(etName); + // 获取编辑文本对象中的内容,并转换为字符串 + String name = etName.getText().toString(); + // 调用数据工具类的方法,检查该文件夹名称是否已经存在,如果存在,就弹出一个提示信息,并选中编辑文本对象中的内容,然后返回 + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + etName.setSelection(0, etName.length()); + return; + } + // 如果不是创建,就执行以下操作 + if (!create) { + // 如果文件夹名称不为空,就创建一个内容值对象,并设置其摘要、类型和本地修改标志 + if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + // 调用内容解析器对象,更新对应的笔记项数据对象 + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + // 如果是创建,并且文件夹名称不为空,就创建一个内容值对象,并设置其摘要和类型 + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 调用内容解析器对象,插入一个新的笔记项数据对象 + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + // 关闭对话框 + dialog.dismiss(); + } + }); + + // 如果编辑文本对象中的内容为空,就设置确定按钮为不可用 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + /** + * When the name edit text is null, disable the positive button + */ + // 给编辑文本对象添加一个文本变化监听器,用来实时更新确定按钮的状态 + etName.addTextChangedListener(new TextWatcher() { + // 这个方法是在文本变化之前调用的,这里没有实现任何功能 + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + + } + + // 这个方法是在文本变化时调用的 + public void onTextChanged(CharSequence s, int start, int before, int count) { + // 如果编辑文本对象中的内容为空,就设置确定按钮为不可用,否则设置为可用 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + // 这个方法是在文本变化之后调用的,这里没有实现任何功能 + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + // 这个方法是在用户按下返回键时调用的 + public void onBackPressed() { + // 根据当前的列表编辑状态,执行相应的操作 + switch (mState) { + // 如果是子文件夹状态,就把当前文件夹的ID设为根文件夹的ID,并把列表编辑状态设为笔记列表状态,然后开始异步查询笔记列表,并隐藏标题栏 + case SUB_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + startAsyncNotesListQuery(); + mTitleBar.setVisibility(View.GONE); + break; + // 如果是通话记录文件夹状态,就把当前文件夹的ID设为根文件夹的ID,并把列表编辑状态设为笔记列表状态,然后开始异步查询笔记列表,并显示添加新笔记按钮,并隐藏标题栏 + case CALL_RECORD_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + mAddNewNote.setVisibility(View.VISIBLE); + mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + // 如果是笔记列表状态,就调用父类的方法 + case NOTE_LIST: + super.onBackPressed(); + break; + // 如果是其他状态,就什么都不做 + default: + break; + } + } + + // 这个方法是更新小部件的,传入一个小部件的ID和类型 + private void updateWidget(int appWidgetId, int appWidgetType) { + // 创建一个意图对象,并设置其动作为小部件更新 + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据小部件的类型,设置意图对象的类为对应的小部件提供器类 + if (appWidgetType == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + // 如果不支持该类型的小部件,就打印错误日志,并返回 + Log.e(TAG, "Unspported widget type"); + return; + } + + // 把小部件的ID作为额外数据传给意图对象 + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + + // 发送广播,通知小部件更新,并设置结果为成功 + sendBroadcast(intent); + setResult(RESULT_OK, intent); + } + + // 这个变量是一个创建上下文菜单的监听器对象,用来处理文件夹的上下文菜单 + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + // 这个方法是在创建上下文菜单时调用的,传入一个菜单对象、一个视图对象和一个菜单信息对象 + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + // 如果当前的焦点笔记项数据对象不为空,就执行以下操作 + if (mFocusNoteDataItem != null) { + // 设置菜单的标题为焦点笔记项数据对象的摘要 + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + // 添加三个菜单项,分别是查看文件夹、删除文件夹和修改文件夹名称,并设置其ID和标题 + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } + }; + + @Override + // 这个方法是在上下文菜单关闭时调用的,传入一个菜单对象 + public void onContextMenuClosed(Menu menu) { + // 如果列表视图对象不为空,就设置其创建上下文菜单的监听器为null + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + // 调用父类的方法 + super.onContextMenuClosed(menu); + } + @Override + + // 这个方法是在选择上下文菜单中的某一项时调用的,传入一个菜单项对象 + public boolean onContextItemSelected(MenuItem item) { + // 如果当前的焦点笔记项数据对象为空,就打印错误日志,并返回false + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + // 根据菜单项的ID,执行相应的操作 + switch (item.getItemId()) { + // 如果是查看文件夹菜单项,就调用打开文件夹的方法,传入焦点笔记项数据对象 + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem); + break; + // 如果是删除文件夹菜单项,就弹出一个确认对话框,询问用户是否要删除该文件夹 + case MENU_FOLDER_DELETE: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + // 如果用户点击确定,就调用删除文件夹的方法,传入焦点笔记项数据对象的ID + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + // 如果用户点击取消,就什么都不做 + builder.setNegativeButton(android.R.string.cancel, null); + // 显示对话框 + builder.show(); + break; + // 如果是修改文件夹名称菜单项,就调用显示创建或修改文件夹对话框的方法,传入false表示不是创建 + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + // 如果是其他菜单项,就什么都不做 + default: + break; + } + + // 返回true表示处理了该事件 + return true; + } + + @Override + // 这个方法是在准备选项菜单时调用的,传入一个菜单对象 + public boolean onPrepareOptionsMenu(Menu menu) { + // 清空菜单对象中的所有菜单项 + menu.clear(); + // 根据当前的列表编辑状态,执行相应的操作 + if (mState == ListEditState.NOTE_LIST) { + // 如果是笔记列表状态,就从资源文件中加载笔记列表菜单,并设置同步或取消同步菜单项的标题 + getMenuInflater().inflate(R.menu.note_list, menu); + // set sync or sync_cancel + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + // 如果是子文件夹状态,就从资源文件中加载子文件夹菜单 + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + // 如果是通话记录文件夹状态,就从资源文件中加载通话记录文件夹菜单 + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + // 如果是其他状态,就打印错误日志 + Log.e(TAG, "Wrong state:" + mState); + } + // 返回true表示准备好了选项菜单 + return true; + } + + @Override + // 这个方法是在选择选项菜单中的某一项时调用的,传入一个菜单项对象 + public boolean onOptionsItemSelected(MenuItem item) { + // 根据菜单项的ID,执行相应的操作 + switch (item.getItemId()) { + // 如果是新建文件夹菜单项,就调用显示创建或修改文件夹对话框的方法,传入true表示是创建 + case R.id.menu_new_folder: { + showCreateOrModifyFolderDialog(true); + break; + } + // 如果是导出文本菜单项,就调用导出笔记到文本的方法 + case R.id.menu_export_text: { + exportNoteToText(); + break; + } + // 如果是同步或取消同步菜单项,就判断是否是同步模式 + case R.id.menu_sync: { + if (isSyncMode()) { + // 如果是同步模式,就判断菜单项的标题是否是同步,如果是,就调用同步服务类的方法,开始同步 + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + // 否则,就调用同步服务类的方法,取消同步 + GTaskSyncService.cancelSync(this); + } + } else { + // 如果不是同步模式,就调用启动偏好设置Activity的方法 + startPreferenceActivity(); + } + break; + } + // 如果是设置菜单项,就调用启动偏好设置Activity的方法 + case R.id.menu_setting: { + startPreferenceActivity(); + break; + } + // 如果是新建笔记菜单项,就调用创建新笔记的方法 + case R.id.menu_new_note: { + createNewNote(); + break; + } + // 如果是搜索菜单项,就调用启动搜索请求的方法 + case R.id.menu_search: + onSearchRequested(); + break; + // 如果是其他菜单项,就什么都不做 + default: + break; + } + // 返回true表示处理了该事件 + return true; + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null /* appData */, false); + return true; + } + + private void exportNoteToText() { + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + @Override + // 这个方法是在异步任务执行完毕后调用的 + protected void onPostExecute(Integer result) { + // 根据结果的不同,显示不同的对话框 + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // 如果结果是SD卡未挂载,就创建一个警告对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题为导出失败 + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + // 设置对话框的内容为SD卡未挂载的错误信息 + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_unmounted)); + // 设置对话框的确定按钮,点击后关闭对话框 + builder.setPositiveButton(android.R.string.ok, null); + // 显示对话框 + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + // 如果结果是成功,就创建一个提示对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题为导出成功 + builder.setTitle(NotesListActivity.this + .getString(R.string.success_sdcard_export)); + // 设置对话框的内容为导出文件的位置和名称 + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup + .getExportedTextFileName(), backup.getExportedTextFileDir())); + // 设置对话框的确定按钮,点击后关闭对话框 + builder.setPositiveButton(android.R.string.ok, null); + // 显示对话框 + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + // 如果结果是系统错误,就创建一个警告对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题为导出失败 + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + // 设置对话框的内容为系统错误的信息 + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_export)); + // 设置对话框的确定按钮,点击后关闭对话框 + builder.setPositiveButton(android.R.string.ok, null); + // 显示对话框 + builder.show(); + } + } + +// 执行异步任务 + }.execute(); + } + + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + private void startPreferenceActivity() { + Activity from = getParent() != null ? getParent() : this; + Intent intent = new Intent(from, NotesPreferenceActivity.class); + from.startActivityIfNeeded(intent, -1); + } + + // 这个内部类实现了列表项点击监听器的接口,用来处理列表视图中的点击事件 + private class OnListItemClickListener implements OnItemClickListener { + + // 这个方法是在点击列表视图中的某一项时调用的,传入一个父视图对象、一个被点击的视图对象、一个位置和一个ID + public void onItemClick(AdapterView parent, View view, int position, long id) { + // 如果被点击的视图对象是一个笔记列表项对象,就执行以下操作 + if (view instanceof NotesListItem) { + // 获取笔记列表项对象中的笔记项数据对象 + NoteItemData item = ((NotesListItem) view).getItemData(); + // 判断列表适配器是否处于选择模式 + if (mNotesListAdapter.isInChoiceMode()) { + // 如果是选择模式,并且笔记项数据对象的类型是笔记类型,就执行以下操作 + if (item.getType() == Notes.TYPE_NOTE) { + // 计算出被点击的位置(减去列表视图的头部视图数量),并调用操作模式回调对象的方法,改变该位置的选中状态 + position = position - mNotesListView.getHeaderViewsCount(); + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + // 返回,不执行后面的操作 + return; + } + + // 根据当前的列表编辑状态,执行相应的操作 + switch (mState) { + // 如果是笔记列表状态,就判断笔记项数据对象的类型 + case NOTE_LIST: + // 如果是文件夹类型或系统类型,就调用打开文件夹的方法,传入笔记项数据对象 + if (item.getType() == Notes.TYPE_FOLDER + || item.getType() == Notes.TYPE_SYSTEM) { + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + // 如果是笔记类型,就调用打开笔记节点的方法,传入笔记项数据对象 + openNode(item); + } else { + // 如果是其他类型,就打印错误日志 + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + // 如果是子文件夹状态或通话记录文件夹状态,就判断笔记项数据对象的类型 + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + // 如果是笔记类型,就调用打开笔记节点的方法,传入笔记项数据对象 + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + // 如果是其他类型,就打印错误日志 + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + // 如果是其他状态,就什么都不做 + default: + break; + } + } + } + + } + + // 这个方法是开始查询目标文件夹的 + private void startQueryDestinationFolders() { + // 设置查询条件,筛选出类型为文件夹,并且父ID不是回收站文件夹,并且ID不是当前文件夹的笔记项 + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + // 如果当前的列表编辑状态是笔记列表状态,就保持原来的查询条件,否则就加上根文件夹的ID + selection = (mState == ListEditState.NOTE_LIST) ? selection: + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + // 调用后台查询处理器对象,开始查询笔记的数据,并指定查询标识符、投影、条件和排序方式 + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + selection, + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); + } + + // 这个方法是实现列表项长按监听器的接口,用来处理列表视图中的长按事件 + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + // 如果被长按的视图对象是一个笔记列表项对象,就执行以下操作 + if (view instanceof NotesListItem) { + // 获取笔记列表项对象中的笔记项数据对象,并赋值给当前的焦点笔记项数据对象 + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + // 判断焦点笔记项数据对象的类型是否是笔记类型,并且列表适配器是否不处于选择模式 + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + // 如果是,就调用列表视图对象的方法,启动操作模式,并传入操作模式回调对象 + if (mNotesListView.startActionMode(mModeCallBack) != null) { + // 如果启动成功,就调用操作模式回调对象的方法,改变被长按位置的选中状态,并执行触觉反馈 + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + // 如果启动失败,就打印错误日志 + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 如果焦点笔记项数据对象的类型是文件夹类型,就设置列表视图对象的创建上下文菜单的监听器为文件夹创建上下文菜单的监听器 + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + // 返回false表示没有处理该事件 + return false; + } +} diff --git a/doc/精读报告/widget/NoteWidgetProvider.doc b/doc/精读报告/widget/NoteWidgetProvider.doc new file mode 100644 index 0000000..364842f --- /dev/null +++ b/doc/精读报告/widget/NoteWidgetProvider.doc @@ -0,0 +1,169 @@ +/* + * 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.widget; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +public abstract class NoteWidgetProvider extends AppWidgetProvider { + //查询便签数据库时使用的列名 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + //PROJECTION中对应列的下标 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + //Log输出的标签 + private static final String TAG = "NoteWidgetProvider"; + + /* + * 重写onDeleted()方法,实现删除 Widget 时清除相应 Widget 对应的便签数据库中的记录 + * value:用于更新的ContentValues,将 Widget ID 设为无效值 + * appWidgetIds:被删除的所有 Widget 的 ID 数组 + */ + @Override +// 定义一个方法,用于在widget被删除时更新数据库中的widget_id字段 + public void onDeleted(Context context, int[] appWidgetIds) { + // 创建一个ContentValues对象,用于存放要更新的字段和值 + ContentValues values = new ContentValues(); + // 把widget_id设置为无效的值 + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + //遍历所有被删除的widget的id + for (int i = 0; i < appWidgetIds.length; i++) { + // 根据widget_id更新数据库中对应的笔记记录 + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + +// 定义一个方法,用于根据widget_id查询数据库中的笔记信息 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + // 使用ContentResolver查询笔记表,返回一个Cursor对象 + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + +// 定义一个方法,用于更新widget的视图 + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用另一个重载的update方法,传入false表示不是访客模式 + update(context, appWidgetManager, appWidgetIds, false); + } + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 获取 Widget 的默认背景 ID + int bgId = ResourceParser.getDefaultBgId(context); + // 默认便签摘要为空字符串 + String snippet = ""; + // 创建用于启动编辑页面的 Intent,并添加必要的参数 + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 从数据库中查询指定 Widget ID 对应的便签信息 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + // 检查查询结果是否出现异常,如出现异常则打印错误日志和异常信息 + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 设置 Widget 中需要显示的便签摘要、背景 ID、绑定的便签 ID 和点击跳转功能 + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + // 若查询结果为空,则打开编辑页面并设置其模式为插入模式 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 关闭查询结果的 Cursor 对象 + if (c != null) { + c.close(); + } + + // 创建 RemoteViews 对象,并为其设置相应布局及需要显示的参数 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + /** + * Generate the pending intent to start host for the widget + */ + // 定义一个变量pendingIntent,初始化为null + PendingIntent pendingIntent = null; +// 如果privacyMode为真,表示用户处于访客模式 + if (privacyMode) { + // 设置widget_text的文本为“访客模式” + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + // 创建一个PendingIntent,用于启动NotesListActivity + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 否则,设置widget_text的文本为snippet,即笔记的摘要 + rv.setTextViewText(R.id.widget_text, snippet); + // 创建一个PendingIntent,用于启动intent指定的Activity + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + +// 设置widget_text的点击事件为pendingIntent + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); +// 更新widget的视图 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + +// 定义一个抽象方法,用于根据bgId返回背景资源的id + protected abstract int getBgResourceId(int bgId); + +// 定义一个抽象方法,用于返回布局资源的id + protected abstract int getLayoutId(); + +// 定义一个抽象方法,用于返回widget的类型 + protected abstract int getWidgetType(); +} diff --git a/doc/精读报告/widget/NoteWidgetProvider_2x.doc b/doc/精读报告/widget/NoteWidgetProvider_2x.doc new file mode 100644 index 0000000..9933e13 --- /dev/null +++ b/doc/精读报告/widget/NoteWidgetProvider_2x.doc @@ -0,0 +1,48 @@ +/* + * 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.widget; //声明package + +import android.appwidget.AppWidgetManager; //引用类 +import android.content.Context; //引用类 + +import net.micode.notes.R; //引用类 +import net.micode.notes.data.Notes; //引用类 +import net.micode.notes.tool.ResourceParser; //引用类 + +public class NoteWidgetProvider_2x extends NoteWidgetProvider {//继承NoteWidgetProvider类 + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); //调用NoteWidgetProvider的update方法 + } + + @Override + protected int getLayoutId() { //重写NoteWidgetProvider中的getLayoutId方法 + return R.layout.widget_2x; //返回 widget_2x 布局文件的ID + } + + @Override + protected int getBgResourceId(int bgId) { //重写NoteWidgetProvider中的getBgResourceId方法,接受传入的bgId参数 + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); //调用ResourceParser类中的getWidget2xBgResource(bgId)方法,返回该bgId对应的Widget2xBg资源ID + } + + @Override + protected int getWidgetType() { //重写NoteWidgetProvider中的getWidgetType方法 + return Notes.TYPE_WIDGET_2X; //返回Note中的TYPE_WIDGET_2X常量值 + } +} + diff --git a/doc/精读报告/widget/NoteWidgetProvider_4x.doc b/doc/精读报告/widget/NoteWidgetProvider_4x.doc new file mode 100644 index 0000000..17222a0 --- /dev/null +++ b/doc/精读报告/widget/NoteWidgetProvider_4x.doc @@ -0,0 +1,58 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + + +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类中的 update 方法,进行具体的 Widget 内容更新操作 + super.update(context, appWidgetManager, appWidgetIds); + } + + + protected int getLayoutId() { + return R.layout.widget_4x; + } + + /** + * 根据传入的背景颜色 ID 获取对应的背景资源 ID + * @param bgId 背景颜色 ID + * @return 对应的背景资源 ID + */ + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + /** + * 获取该 Widget 对应的类型常量,用于在程序中进行判断和处理 + * @return 4x4 Widget 对应的类型常量 + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } +}