diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d90aa1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*/~$* \ No newline at end of file diff --git a/doc/a.txt b/doc/a.txt deleted file mode 100644 index e69de29..0000000 diff --git a/doc/小米便签质量分析报告.docx b/doc/小米便签质量分析报告.docx new file mode 100644 index 0000000..0acd732 Binary files /dev/null and b/doc/小米便签质量分析报告.docx differ diff --git a/doc/开源软件泛读、标注和维护报告文档2024.docx b/doc/开源软件泛读、标注和维护报告文档2024.docx new file mode 100644 index 0000000..d24bfa0 Binary files /dev/null and b/doc/开源软件泛读、标注和维护报告文档2024.docx differ diff --git a/src/main/java/net/micode/notes/data/Contact.java b/src/main/java/net/micode/notes/data/Contact.java index d97ac5d..d986777 100644 --- a/src/main/java/net/micode/notes/data/Contact.java +++ b/src/main/java/net/micode/notes/data/Contact.java @@ -36,38 +36,59 @@ public class Contact { + " FROM phone_lookup" + " WHERE min_match = '+')"; - public static String getContact(Context context, String phoneNumber) { - if(sContactCache == null) { - sContactCache = new HashMap(); - } +/** + * 根据电话号码获取联系人名称 + * 该方法首先检查缓存中是否已存在与给定电话号码关联的联系人名称, + * 如果不存在,则通过查询系统联系人数据获取,并将结果缓存 + * + * @param context 应用程序上下文,用于访问系统联系人数据 + * @param phoneNumber 电话号码,用于查询联系人 + * @return 与给定电话号码关联的联系人名称,如果找不到则返回null + */ +public static String getContact(Context context, String phoneNumber) { + // 检查缓存是否已初始化,未初始化则进行初始化 + if(sContactCache == null) { + sContactCache = new HashMap(); + } - if(sContactCache.containsKey(phoneNumber)) { - return sContactCache.get(phoneNumber); - } + // 检查缓存中是否含有指定电话号码的联系人名称,如果有直接返回 + if(sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + + // 构建查询选型字符串,用于匹配电话号码 + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); - String selection = CALLER_ID_SELECTION.replace("+", - PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); - Cursor cursor = context.getContentResolver().query( - Data.CONTENT_URI, - new String [] { Phone.DISPLAY_NAME }, - selection, - new String[] { phoneNumber }, - null); + // 通过系统接口查询匹配的联系人信息 + Cursor cursor = context.getContentResolver().query( + Data.CONTENT_URI, + new String [] { Phone.DISPLAY_NAME }, + selection, + new String[] { phoneNumber }, + null); - if (cursor != null && cursor.moveToFirst()) { - try { - String name = cursor.getString(0); - sContactCache.put(phoneNumber, name); - return name; - } catch (IndexOutOfBoundsException e) { - Log.e(TAG, " Cursor get string error " + e.toString()); - return null; - } finally { - cursor.close(); - } - } else { - Log.d(TAG, "No contact matched with number:" + phoneNumber); + // 如果查询到匹配的联系人信息 + if (cursor != null && cursor.moveToFirst()) { + try { + // 获取联系人名称 + String name = cursor.getString(0); + // 将名称缓存 + sContactCache.put(phoneNumber, name); + // 返回联系人名称 + return name; + } catch (IndexOutOfBoundsException e) { + // 如果出现索引越界异常,记录日志并返回null + Log.e(TAG, " Cursor get string error " + e.toString()); return null; + } finally { + // 关闭游标,释放资源 + cursor.close(); } + } else { + // 如果没有匹配的联系人,记录日志并返回null + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; } } +} diff --git a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java index ffe5d57..78c6ef5 100644 --- a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java @@ -210,10 +210,23 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { super(context, DB_NAME, null, DB_VERSION); } + /** + * 创建笔记数据库表 + * + * @param db 数据库对象,用于执行SQL语句 + * + * 此方法负责在数据库中创建用于存储笔记信息的表它首先执行一条SQL语句来创建表结构, + * 然后调用其他方法来重新创建与表相关的触发器,并创建系统文件夹这些操作确保了笔记表 + * 准备就绪,可以正常使用 + */ public void createNoteTable(SQLiteDatabase db) { + // 执行SQL语句以创建笔记表 db.execSQL(CREATE_NOTE_TABLE_SQL); + // 重新创建笔记表的触发器 reCreateNoteTableTriggers(db); + // 创建系统文件夹,确保系统笔记有存储位置 createSystemFolder(db); + // 记录日志,表明笔记表已成功创建 Log.d(TAG, "note table has been created"); } diff --git a/src/main/java/net/micode/notes/data/NotesProvider.java b/src/main/java/net/micode/notes/data/NotesProvider.java index edb0a60..ea6dedd 100644 --- a/src/main/java/net/micode/notes/data/NotesProvider.java +++ b/src/main/java/net/micode/notes/data/NotesProvider.java @@ -85,206 +85,314 @@ public class NotesProvider extends ContentProvider { return true; } - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - Cursor c = null; - SQLiteDatabase db = mHelper.getReadableDatabase(); - String id = null; - switch (mMatcher.match(uri)) { - case URI_NOTE: - c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, - sortOrder); - break; - case URI_NOTE_ITEM: - id = uri.getPathSegments().get(1); - c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id - + parseSelection(selection), selectionArgs, null, null, sortOrder); - break; - case URI_DATA: - c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, - sortOrder); - break; - case URI_DATA_ITEM: - id = uri.getPathSegments().get(1); - c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id - + parseSelection(selection), selectionArgs, null, null, sortOrder); - break; - case URI_SEARCH: - case URI_SEARCH_SUGGEST: - if (sortOrder != null || projection != null) { - throw new IllegalArgumentException( - "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); - } +@Override +/** + * 重写query方法,根据不同的URI查询数据库并返回光标 + * + * @param uri 访问的URI + * @param projection 需要查询的列数组 + * @param selection 用于where子句的筛选条件 + * @param selectionArgs 用于where子句的筛选条件参数 + * @param sortOrder 排序顺序 + * @return 返回查询结果的光标,如果没有查询到数据则返回null + */ +public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Cursor c = null; + // 获取可读的数据库实例 + SQLiteDatabase db = mHelper.getReadableDatabase(); + // 用于存储URI中的id部分,如果有的话 + String id = null; + // 根据URI类型执行不同的查询 + switch (mMatcher.match(uri)) { + case URI_NOTE: + // 查询NOTE表 + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM: + // 从URI路径段中获取note的id + id = uri.getPathSegments().get(1); + // 查询NOTE表中的特定id项 + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: + // 查询DATA表 + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: + // 从URI路径段中获取data的id + id = uri.getPathSegments().get(1); + // 查询DATA表中的特定id项 + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + // 确保没有指定排序顺序和投影,因为这是搜索专用查询 + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } - String searchString = null; - if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { - if (uri.getPathSegments().size() > 1) { - searchString = uri.getPathSegments().get(1); - } - } else { - searchString = uri.getQueryParameter("pattern"); + // 初始化搜索字符串 + String searchString = null; + // 根据URI类型获取搜索字符串 + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); } + } else { + searchString = uri.getQueryParameter("pattern"); + } - if (TextUtils.isEmpty(searchString)) { - return null; - } + // 如果搜索字符串为空,则返回null + if (TextUtils.isEmpty(searchString)) { + return null; + } - try { - searchString = String.format("%%%s%%", searchString); - c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, - new String[] { searchString }); - } catch (IllegalStateException ex) { - Log.e(TAG, "got exception: " + ex.toString()); - } - break; - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - if (c != null) { - c.setNotificationUri(getContext().getContentResolver(), uri); - } - return c; + try { + // 格式化搜索字符串,添加通配符 + searchString = String.format("%%%s%%", searchString); + // 执行原始查询以获取搜索结果 + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, + new String[] { searchString }); + } catch (IllegalStateException ex) { + // 记录查询异常 + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + default: + // 如果URI不匹配任何已知类型,抛出非法参数异常 + throw new IllegalArgumentException("Unknown URI " + uri); + } + // 如果返回的光标不为空,设置通知URI,以便数据变化时可以发送通知 + if (c != null) { + c.setNotificationUri(getContext().getContentResolver(), uri); } + // 返回查询结果光标 + return c; +} @Override + /** + * 向数据库插入数据,并根据插入操作的结果更新URI。 + * + * @param uri 要插入数据的URI。 + * @param values 要插入的数据,存储在ContentValues对象中。 + * @return 插入操作后更新的URI。 + */ public Uri insert(Uri uri, ContentValues values) { + // 获取可写数据库实例 SQLiteDatabase db = mHelper.getWritableDatabase(); + // 初始化数据ID和笔记ID为0 long dataId = 0, noteId = 0, insertedId = 0; + // 根据URI类型执行不同的插入操作 switch (mMatcher.match(uri)) { + // 处理笔记URI case URI_NOTE: + // 执行插入操作并获取新记录的ID insertedId = noteId = db.insert(TABLE.NOTE, null, values); break; + // 处理数据URI case URI_DATA: + // 检查ContentValues中是否包含笔记ID if (values.containsKey(DataColumns.NOTE_ID)) { + // 获取笔记ID noteId = values.getAsLong(DataColumns.NOTE_ID); } else { + // 如果没有笔记ID,则记录错误信息 Log.d(TAG, "Wrong data format without note id:" + values.toString()); } + // 执行插入操作并获取新记录的ID insertedId = dataId = db.insert(TABLE.DATA, null, values); break; + // 默认情况下,抛出异常 default: throw new IllegalArgumentException("Unknown URI " + uri); } - // Notify the note uri + // 如果笔记ID有效,则通知相应的URI发生了改变 if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } - // Notify the data uri + // 如果数据ID有效,则通知相应的URI发生了改变 if (dataId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); } + // 返回更新后的URI,附加新插入数据的ID return ContentUris.withAppendedId(uri, insertedId); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { + // 初始化删除记录数 int count = 0; + // 用于存储ID String id = null; + // 获取可写数据库实例 SQLiteDatabase db = mHelper.getWritableDatabase(); + // 标记是否删除了数据 boolean deleteData = false; + // 根据URI类型执行不同的删除操作 switch (mMatcher.match(uri)) { case URI_NOTE: + // 对于URI_NOTE,添加附加的删除条件 selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + // 执行删除操作 count = db.delete(TABLE.NOTE, selection, selectionArgs); break; case URI_NOTE_ITEM: + // 获取ID id = uri.getPathSegments().get(1); - /** - * ID that smaller than 0 is system folder which is not allowed to - * trash - */ + // 小于等于0的ID是系统文件夹,不允许删除 long noteId = Long.valueOf(id); if (noteId <= 0) { break; } + // 构造删除语句并执行 count = db.delete(TABLE.NOTE, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); break; case URI_DATA: + // 执行数据删除 count = db.delete(TABLE.DATA, selection, selectionArgs); + // 标记数据已删除 deleteData = true; break; case URI_DATA_ITEM: + // 获取ID id = uri.getPathSegments().get(1); + // 构造删除语句并执行 count = db.delete(TABLE.DATA, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + // 标记数据已删除 deleteData = true; break; default: + // 对于未知URI,抛出异常 throw new IllegalArgumentException("Unknown URI " + uri); } + // 如果有数据被删除,则通知系统更新UI if (count > 0) { if (deleteData) { + // 如果删除了数据,则通知系统刷新Notes.CONTENT_NOTE_URI对应的UI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } + // 通知系统刷新被删除数据对应的UI getContext().getContentResolver().notifyChange(uri, null); } + // 返回删除的记录数 return count; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + // 初始化更新数量和笔记ID int count = 0; String id = null; + // 获取可写数据库实例 SQLiteDatabase db = mHelper.getWritableDatabase(); + // 标记是否更新了数据表 boolean updateData = false; + + // 根据URI类型执行不同的更新操作 switch (mMatcher.match(uri)) { case URI_NOTE: + // 在更新笔记前增加版本号 increaseNoteVersion(-1, selection, selectionArgs); + // 更新笔记表 count = db.update(TABLE.NOTE, values, selection, selectionArgs); break; case URI_NOTE_ITEM: + // 获取具体笔记ID id = uri.getPathSegments().get(1); + // 增加指定笔记的版本号 increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); + // 更新笔记表,指定ID count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); break; case URI_DATA: + // 更新数据表 count = db.update(TABLE.DATA, values, selection, selectionArgs); updateData = true; break; case URI_DATA_ITEM: + // 获取具体数据ID id = uri.getPathSegments().get(1); + // 更新数据表,指定ID count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); updateData = true; break; default: + // 抛出异常,表示未知的URI throw new IllegalArgumentException("Unknown URI " + uri); } + // 如果有数据被更新,则发送内容改变广播 if (count > 0) { if (updateData) { + // 如果更新了数据表,则发送数据表改变广播 getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } + // 发送URI对应的数据改变广播 getContext().getContentResolver().notifyChange(uri, null); } + // 返回更新的行数 return count; } + /** + * 解析选择条件,用于在数据库查询时添加额外的筛选条件 + * + * @param selection 用户传入的选择条件字符串,可能为null或空 + * @return 返回格式化后的选择条件,如果输入为null或空则返回空字符串 + * + * 此方法的目的在于安全地添加额外的选择条件到数据库查询语句中 + * 如果传入的选择条件为null或空,則返回空字符串,避免在查询语句中引入无效的AND操作符 + */ private String parseSelection(String selection) { return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); } + /** + * 增加笔记的版本号 + * + * @param id 笔记的ID,用于精确指定要更新的笔记 + * @param selection 选择条件的字符串,用于模糊匹配要更新的笔记 + * @param selectionArgs 选择条件的参数数组,用于替换选择条件中的占位符 + */ private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + // 创建一个字符串构建器来拼接SQL语句 StringBuilder sql = new StringBuilder(120); + + // 拼接SQL的UPDATE部分 sql.append("UPDATE "); sql.append(TABLE.NOTE); sql.append(" SET "); + + // 拼接SQL的SET部分,将版本号加1 sql.append(NoteColumns.VERSION); sql.append("=" + NoteColumns.VERSION + "+1 "); + // 根据ID和选择条件决定是否添加WHERE子句 if (id > 0 || !TextUtils.isEmpty(selection)) { sql.append(" WHERE "); } + + // 如果ID大于0,则直接指定ID作为WHERE子句的一部分 if (id > 0) { sql.append(NoteColumns.ID + "=" + String.valueOf(id)); } + + // 如果有选择条件,则解析选择条件并替换占位符 if (!TextUtils.isEmpty(selection)) { String selectString = id > 0 ? parseSelection(selection) : selection; for (String args : selectionArgs) { @@ -293,6 +401,7 @@ public class NotesProvider extends ContentProvider { sql.append(selectString); } + // 执行SQL语句,更新笔记的版本号 mHelper.getWritableDatabase().execSQL(sql.toString()); } diff --git a/src/main/java/net/micode/notes/tool/BackupUtils.java b/src/main/java/net/micode/notes/tool/BackupUtils.java index 39f6ec4..6d9337f 100644 --- a/src/main/java/net/micode/notes/tool/BackupUtils.java +++ b/src/main/java/net/micode/notes/tool/BackupUtils.java @@ -137,46 +137,60 @@ public class BackupUtils { } /** - * Export the folder identified by folder id to text + * 将指定文件夹导出为文本 + * + * @param folderId 要导出的文件夹的ID + * @param ps 输出流,用于打印导出的内容 */ 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 + * 将指定便笺导出为文本 + * + * @param noteId 便笺ID + * @param ps 打印流,用于输出便笺内容 */ 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 { + // 获取当前行的MIME类型 String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + // 根据MIME类型处理数据 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); @@ -185,16 +199,17 @@ public class BackupUtils { 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), @@ -203,9 +218,10 @@ public class BackupUtils { } } while (dataCursor.moveToNext()); } + // 关闭Cursor dataCursor.close(); } - // print a line separator between note + // 在便笺之间打印一行分隔符 try { ps.write(new byte[] { Character.LINE_SEPARATOR, Character.LETTER_NUMBER @@ -215,21 +231,29 @@ public class BackupUtils { } } + /** - * Note will be exported as text which is user readable + * 将数据导出到文本文件 + * 此方法首先检查外部存储是否可用,然后尝试获取PrintStream对象用于导出 + * 随后导出文件夹及其笔记到文本文件中 + * + * @return 导出操作的状态,可能的值包括STATE_SD_CARD_UNMOUONTED(SD卡未挂载)、STATE_SYSTEM_ERROR(系统错误)、STATE_SUCCESS(成功) */ public int exportToText() { + // 检查外部存储是否可用 if (!externalStorageAvailable()) { Log.d(TAG, "Media was not mounted"); return STATE_SD_CARD_UNMOUONTED; } + // 获取用于导出的PrintStream对象 PrintStream ps = getExportToTextPrintStream(); if (ps == null) { Log.e(TAG, "get print stream error"); return STATE_SYSTEM_ERROR; } - // First export folder and its notes + + // 导出文件夹及其笔记 Cursor folderCursor = mContext.getContentResolver().query( Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, @@ -240,9 +264,9 @@ public class BackupUtils { if (folderCursor != null) { if (folderCursor.moveToFirst()) { do { - // Print folder's name + // 打印文件夹名称 String folderName = ""; - if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { + 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); @@ -257,20 +281,21 @@ public class BackupUtils { 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 + 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)))); - // Query data belong to this note + // 查询属于此笔记的数据 String noteId = noteCursor.getString(NOTE_COLUMN_ID); exportNoteToText(noteId, ps); } while (noteCursor.moveToNext()); @@ -282,61 +307,91 @@ public class BackupUtils { return STATE_SUCCESS; } + /** - * Get a print stream pointed to the file {@generateExportedTextFile} + * 获取用于导出文本的PrintStream对象 + * 该方法尝试在SD卡上生成一个文本文件,并返回一个可以用于向该文件输出文本的PrintStream对象 + * + * @return PrintStream对象,用于向文件输出文本;如果创建文件失败或出现异常,则返回null */ private PrintStream getExportToTextPrintStream() { + // 在SD卡上生成文件 File file = generateFileMountedOnSDcard(mContext, R.string.file_path, R.string.file_name_txt_format); if (file == null) { + // 如果文件创建失败,则记录错误并返回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,并基于它创建PrintStream对象 FileOutputStream fos = new FileOutputStream(file); ps = new PrintStream(fos); } catch (FileNotFoundException e) { + // 如果文件未找到,则记录异常并返回null e.printStackTrace(); return null; } catch (NullPointerException e) { + // 如果出现空指针异常,则记录异常并返回null e.printStackTrace(); return null; } + // 返回创建的PrintStream对象 return ps; } } /** - * Generate the text file to store imported data + * 在SD卡挂载点生成文件 + * 此方法旨在指定的文件路径上创建一个文件,该路径位于SD卡的挂载点上 + * 如果文件路径不存在,则创建必要的目录,如果文件不存在,则创建文件 + * + * @param context 上下文对象,用于获取字符串资源和处理文件操作 + * @param filePathResId 文件路径的字符串资源ID + * @param fileNameFormatResId 文件名格式的字符串资源ID,支持日期格式化 + * @return 创建的File对象,如果由于安全异常或IO异常无法创建则返回null */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { + // 使用StringBuilder拼接文件的完整路径 StringBuilder sb = new StringBuilder(); + // 获取并拼接SD卡挂载点的路径 sb.append(Environment.getExternalStorageDirectory()); + // 获取文件路径字符串资源并拼接到sb sb.append(context.getString(filePathResId)); + // 创建文件目录对象 File filedir = new File(sb.toString()); + // 获取文件名格式字符串资源,支持日期格式化,并拼接到sb 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) { + // 处理IO异常,例如文件系统错误 e.printStackTrace(); } + // 如果发生异常,则返回null return null; } } diff --git a/src/main/java/net/micode/notes/tool/DataUtils.java b/src/main/java/net/micode/notes/tool/DataUtils.java index 2a14982..d79e923 100644 --- a/src/main/java/net/micode/notes/tool/DataUtils.java +++ b/src/main/java/net/micode/notes/tool/DataUtils.java @@ -37,57 +37,101 @@ import java.util.HashSet; public class DataUtils { public static final String TAG = "DataUtils"; + /** + * 批量删除便签 + * + * @param resolver ContentResolver对象,用于操作内容提供者 + * @param ids 便签ID的集合 + * @return 如果删除成功返回true,否则返回false + */ public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + // 检查ids是否为null if (ids == null) { Log.d(TAG, "the ids is null"); return true; } + // 检查ids集合是否为空 if (ids.size() == 0) { Log.d(TAG, "no id is in the hashset"); return true; } + // 初始化操作列表用于批量操作 ArrayList operationList = new ArrayList(); + // 遍历ids集合,为每个id构建删除操作 for (long id : ids) { + // 避免删除系统根目录 if(id == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Don't delete system folder root"); continue; } + // 使用ContentProviderOperation构建删除操作,并添加到操作列表中 ContentProviderOperation.Builder builder = ContentProviderOperation .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_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()); 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())); } + // 如果发生异常,则返回false return false; } + /** + * 将便签从一个文件夹移动到另一个文件夹 + * 此方法通过更新便签的父ID、原始父ID和本地修改标志来实现便签的移动 + * + * @param resolver 用于访问便签内容提供者的对象 + * @param id 便签的唯一标识符 + * @param srcFolderId 便签当前所在的源文件夹ID + * @param desFolderId 便签的目标文件夹ID + */ public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { + // 创建一个新的内容值对象,用于存储将要更新的数据 ContentValues values = new ContentValues(); + // 设置便签的新父ID,即将便签移动到的目标文件夹 values.put(NoteColumns.PARENT_ID, desFolderId); + // 记录便签原始所在的文件夹ID,以便后续操作时参考 values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); + // 标记便签的本地修改状态为已修改,用于同步功能 values.put(NoteColumns.LOCAL_MODIFIED, 1); + // 使用提供的ContentResolver来更新便签的信息 + // 通过便签的ID和之前准备的内容值对象来执行更新操作 resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); } + /** + * 将多个便签移动到指定文件夹 + * + * @param resolver 用于操作内容提供者的对象 + * @param ids 需要移动的便签的ID集合 + * @param folderId 目标文件夹的ID + * @return 如果移动成功则返回true,否则返回false + */ public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { + // 检查ids参数是否为null if (ids == null) { Log.d(TAG, "the ids is null"); return true; } + // 初始化操作列表用于批量操作 ArrayList operationList = new ArrayList(); + // 遍历每个便签ID,构建更新操作 for (long id : ids) { ContentProviderOperation.Builder builder = ContentProviderOperation .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); @@ -96,10 +140,12 @@ public class DataUtils { 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()); + Log.d(TAG, "move notes to folder failed, ids:" + ids.toString()); return false; } return true; @@ -111,139 +157,247 @@ public class DataUtils { return false; } + /** - * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + * 通过查询内容提供者的 Uri 来获取用户文件夹的数量 + * + * @param resolver ContentResolver 对象,用于操作内容提供者 + * @return 用户文件夹的数量,不包括垃圾箱文件夹 */ public static int getUserFolderCount(ContentResolver resolver) { - Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, + // 查询用户文件夹的数量,排除垃圾箱文件夹 + 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); int count = 0; - if(cursor != null) { - if(cursor.moveToFirst()) { + 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; } + /** + * 检查笔记是否在笔记数据库中可见。 + * + * 此方法根据笔记ID和类型查询笔记数据库,判断笔记是否可见。主要检查笔记的类型是否符合指定类型且不在回收站文件夹中。 + * + * @param resolver ContentResolver 用于访问笔记数据库。 + * @param noteId 要检查的笔记ID。 + * @param type 要检查的笔记类型。 + * @return 如果笔记可见返回true;否则返回false。 + */ 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); + // 初始化存在标志为false。 boolean exist = false; + // 如果查询结果不为空,处理结果。 if (cursor != null) { + // 如果查询结果包含数据,将exist设置为true。 if (cursor.getCount() > 0) { exist = true; } + // 关闭游标以释放资源。 cursor.close(); } + // 返回判断结果。 return exist; } + /** + * 检查笔记是否存在于数据库中。 + * + * @param resolver ContentResolver 用于查询笔记数据库。 + * @param noteId 要检查的笔记ID。 + * @return 如果笔记存在返回true;否则返回false。 + */ public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + // 根据笔记ID查询数据库以检查笔记是否存在 Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, null, null, null); + // 初始化 exist 标志为 false boolean exist = false; + // 如果查询结果不为空,则处理结果 if (cursor != null) { + // 如果查询返回的行数大于零,则表示笔记存在 if (cursor.getCount() > 0) { exist = true; } + // 关闭游标以释放资源 cursor.close(); } + // 返回笔记是否存在结果 return exist; } + /** + * 检查指定ID的笔记是否存在于数据库中。 + * + * @param resolver 用于查询数据库的ContentResolver + * @param dataId 要查询的笔记ID + * @return 如果笔记存在于数据库中返回true,否则返回false + */ public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + // 根据指定ID查询数据库中的笔记 Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null, null, null, null); + // 初始化是否存在标志为false boolean exist = false; + // 如果查询结果不为空,则处理查询结果 if (cursor != null) { + // 如果查询结果数量大于0,则笔记存在 if (cursor.getCount() > 0) { exist = true; } + // 关闭Cursor以释放资源 cursor.close(); } + // 返回笔记是否存在 return exist; } + /** + * 检查可见文件夹名称是否存在 + * 通过查询内容提供者,检查是否存在指定名称的可见文件夹 + * 可见文件夹是指类型为文件夹且不在回收站中的文件夹 + * + * @param resolver 内容提供者解析器,用于访问内容提供者 + * @param name 待检查的文件夹名称 + * @return 如果存在指定名称的可见文件夹,则返回true;否则返回false + */ public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { + // 查询类型为文件夹且不在回收站中的文件夹,并根据名称进行筛选 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); + // 初始化存在性标志为false boolean exist = false; + // 如果游标不为空,表示查询有结果 if(cursor != null) { + // 如果游标计数大于0,表示存在匹配的文件夹 if(cursor.getCount() > 0) { exist = true; } + // 关闭游标,释放资源 cursor.close(); } + // 返回存在性标志 return exist; } + /** + * 根据文件夹ID获取笔记小部件的集合 + * 该方法通过查询内容提供者来获取属于特定文件夹的所有笔记小部件的属性 + * + * @param resolver 内容解析者,用于访问内容提供者 + * @param folderId 文件夹的ID,用于查询特定文件夹的笔记小部件 + * @return 返回一个HashSet集合,包含指定文件夹中所有笔记小部件的属性 + * 如果没有找到任何笔记小部件或发生错误,则返回null + */ public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + // 查询属于指定文件夹的笔记,基于笔记的内容URI和文件夹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); widget.widgetType = c.getInt(1); + // 将提取的数据添加到集合中 set.add(widget); } catch (IndexOutOfBoundsException e) { + // 捕获并记录可能的查询结果索引越界异常 Log.e(TAG, e.toString()); } } while (c.moveToNext()); } + // 关闭查询结果的游标,释放资源 c.close(); } + // 返回处理结果集合 return set; } + /** + * 根据便条ID获取电话号码 + * 该方法通过查询Notes的内容提供者,来获取与特定便条ID关联的电话号码 + * + * @param resolver ContentResolver对象,用于访问内容提供者 + * @param noteId 便条的ID,用于定位特定的便条记录 + * @return 返回电话号码字符串如果未找到相关记录或出现异常,则返回空字符串 + */ public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + // 查询Notes的内容提供者,返回满足条件的Cursor对象 + // 参数:查询的URI、投影的列、选择条件和对应的参数、分组方式 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); + // 检查Cursor是否有效,并且移动到第一个数据位置 if (cursor != null && cursor.moveToFirst()) { try { + // 返回电话号码,索引0表示查询结果的第一列 return cursor.getString(0); } catch (IndexOutOfBoundsException e) { + // 捕获IndexOutOfBoundsException,记录日志 Log.e(TAG, "Get call number fails " + e.toString()); } finally { + // 确保关闭Cursor以释放资源 cursor.close(); } } + // 如果Cursor为空或查询不到数据,返回空字符串 return ""; } + /** + * 根据电话号码和通话日期获取通话记录的Note ID + * + * @param resolver ContentResolver对象,用于访问ContentProvider提供的数据 + * @param phoneNumber 电话号码 + * @param callDate 通话日期 + * @return 返回通话记录的Note ID,如果找不到则返回0 + */ public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + // 查询通话记录的Note 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(" @@ -251,45 +405,80 @@ public class DataUtils { new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, null); + // 检查查询结果 if (cursor != null) { + // 如果查询结果中存在数据 if (cursor.moveToFirst()) { try { + // 返回第一条记录的Note ID return cursor.getLong(0); } catch (IndexOutOfBoundsException e) { + // 处理索引越界异常 Log.e(TAG, "Get call note id fails " + e.toString()); } } + // 关闭Cursor对象 cursor.close(); } + // 如果查询结果为空或者发生异常,则返回0 return 0; } + /** + * 根据指定的便签ID获取便签的摘要信息 + * + * @param resolver ContentResolver对象,用于访问内容提供者 + * @param noteId 便签的ID + * @return 便签的摘要信息如果便签不存在,则抛出IllegalArgumentException异常 + */ public static String getSnippetById(ContentResolver resolver, long noteId) { + // 查询指定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); } + /** + * 获取格式化后的代码片段字符串 + * 该方法旨在从可能包含多余空格或换行符的字符串中提取干净的代码片段 + * 它去除了前导和尾随的空白字符,并截取第一个换行符之前的部分 + * + * @param snippet 原始的代码片段字符串,可能包含多余的空格或换行符 + * @return 格式化后的代码片段字符串,去除了多余的空白字符和第一个换行符之后的内容 + */ public static String getFormattedSnippet(String snippet) { + // 检查代码片段是否为null if (snippet != null) { + // 去除字符串首尾的空白字符 snippet = snippet.trim(); + // 查找第一个换行符的位置 int index = snippet.indexOf('\n'); + // 如果找到了换行符 if (index != -1) { + // 截取换行符之前的部分 snippet = snippet.substring(0, index); } } + // 返回格式化后的代码片段 return snippet; } } diff --git a/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java b/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..df37ec2 100644 --- a/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java @@ -48,12 +48,17 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD @Override protected void onCreate(Bundle savedInstanceState) { + // 调用父类的onCreate方法 super.onCreate(savedInstanceState); + // 隐藏标题栏 requestWindowFeature(Window.FEATURE_NO_TITLE); + // 获取当前窗口 final Window win = getWindow(); + // 设置窗口标志,使窗口在屏幕锁定时显示 win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + // 如果屏幕未点亮,则添加一系列窗口标志来控制屏幕状态 if (!isScreenOn()) { win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON @@ -61,24 +66,32 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); } + // 获取启动该Activity的Intent Intent intent = getIntent(); + // 尝试从Intent中提取便签ID,并获取便签的预览内容 try { mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果便签预览内容过长,则截取前SNIPPET_PREW_MAX_LEN个字符,并添加提示信息 mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) : mSnippet; } catch (IllegalArgumentException e) { + // 如果发生异常,打印错误信息并返回 e.printStackTrace(); return; } + // 初始化MediaPlayer对象 mPlayer = new MediaPlayer(); + // 检查便签数据库中是否存在该便签 if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 如果存在,显示操作对话框并播放提醒声音 showActionDialog(); playAlarmSound(); } else { + // 如果不存在,关闭该Activity finish(); } } @@ -88,21 +101,34 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD return pm.isScreenOn(); } + /** + * 播放闹钟声音的方法 + * 该方法首先根据设备的默认设置获取闹钟铃声的URI, + * 然后根据设备的静音模式设置调整音频流类型, + * 最后使用MediaPlayer播放闹钟声音,并设置循环播放 + */ private void playAlarmSound() { + // 获取设备的默认闹钟铃声URI Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + // 获取设备的静音模式设置中关于音频流受影响的部分 int silentModeStreams = Settings.System.getInt(getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + // 根据静音模式设置调整音频流类型 if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { mPlayer.setAudioStreamType(silentModeStreams); } else { mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); } try { + // 设置音频数据源为刚才获取的闹钟铃声URI mPlayer.setDataSource(this, url); + // 准备MediaPlayer播放器 mPlayer.prepare(); + // 设置播放器进行循环播放 mPlayer.setLooping(true); + // 开始播放闹钟声音 mPlayer.start(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block @@ -119,26 +145,52 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } } + /** + * 显示操作对话框 + * 这个方法创建并显示一个对话框,对话框的标题是应用名称,内容来自mSnippet变量 + * 如果屏幕处于点亮状态,对话框还会显示一个负向按钮 + * 对话框的正向按钮和负向按钮都使用了当前对象作为点击事件监听器 + * 当对话框被关闭时,会调用OnDismissListener接口,该接口也由当前对象实现 + */ private void showActionDialog() { + // 创建一个对话框构建器 AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框标题为应用名称 dialog.setTitle(R.string.app_name); + // 设置对话框内容为mSnippet变量的值 dialog.setMessage(mSnippet); + // 设置对话框的正向按钮,并使用当前对象作为点击事件监听器 dialog.setPositiveButton(R.string.notealert_ok, this); + // 如果屏幕处于点亮状态,添加一个负向按钮,并使用当前对象作为点击事件监听器 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); } + // 显示对话框,并设置对话框关闭时的监听器为当前对象 dialog.show().setOnDismissListener(this); } + /** + * 处理对话框中的按钮点击事件 + * + * @param dialog 正在显示的对话框实例 + * @param which 点击的按钮,区分是负面按钮还是其他 + */ public void onClick(DialogInterface dialog, int which) { + // 根据点击的按钮类型执行相应的逻辑 switch (which) { + // 如果点击的是对话框的负面按钮 case DialogInterface.BUTTON_NEGATIVE: + // 创建一个指向NoteEditActivity的意图 Intent intent = new Intent(this, NoteEditActivity.class); + // 设置意图动作为查看,表示即将查看或编辑一个便签 intent.setAction(Intent.ACTION_VIEW); + // 将便签的唯一标识符添加到意图中,以便NoteEditActivity可以识别要编辑的便签 intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 启动NoteEditActivity startActivity(intent); break; default: + // 对于其他按钮,默认情况下不执行任何操作 break; } } diff --git a/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java b/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java index f221202..be45d7d 100644 --- a/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java @@ -40,7 +40,10 @@ public class AlarmInitReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + // 获取当前时间戳,用于后续判断备忘录是否需要提醒 long currentDate = System.currentTimeMillis(); + // 通过ContentResolver查询满足条件的备忘录条目 + // 查询条件:未被标记为已提醒且类型为普通备忘录 Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, PROJECTION, NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, @@ -48,17 +51,25 @@ public class AlarmInitReceiver extends BroadcastReceiver { null); if (c != null) { + // 如果有满足条件的备忘录条目 if (c.moveToFirst()) { do { + // 获取备忘录条目的提醒时间戳 long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建一个Intent,用于发送广播到AlarmReceiver Intent sender = new Intent(context, AlarmReceiver.class); + // 将备忘录条目的ID附加到Intent的数据中 sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建一个PendingIntent,用于延迟执行操作 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取AlarmManager服务 AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); + // 设置一个精确的闹钟时间,使用RTC_WAKEUP类型确保设备在必要时唤醒 alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); } while (c.moveToNext()); } + // 关闭Cursor以释放资源 c.close(); } } diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java index e843aec..9629343 100644 --- a/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -149,38 +149,59 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 当活动返回结果时,检查结果是否为成功 + // 只有当返回结果为成功,并且请求码为打开或新建笔记时,才进行数据更新 if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + // 更新笔记列表适配器的数据源为null,触发UI重新拉取数据 mNotesListAdapter.changeCursor(null); } else { + // 其他情况交由父类处理 super.onActivityResult(requestCode, resultCode, data); } } + /** + * 从原始资源中设置应用信息 + * 该方法主要用于首次添加应用时,从raw资源中读取介绍文本,并保存为工作便笺 + */ private void setAppInfoFromRawRes() { + // 获取SharedPreferences实例,用于存储应用的首选项 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + + // 检查是否已经添加过介绍,如果已经添加,则不执行后续操作 if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + // 使用StringBuilder来拼接读取的介绍文本 StringBuilder sb = new StringBuilder(); + + // 定义InputStream实例,用于读取raw资源 InputStream in = null; try { - in = getResources().openRawResource(R.raw.introduction); + // 打开raw资源文件 + in = getResources().openRawResource(R.raw.introduction); if (in != null) { + // 将InputStream包装为InputStreamReader,以便按字符读取 InputStreamReader isr = new InputStreamReader(in); + // 再将InputStreamReader包装为BufferedReader,提高读取效率 BufferedReader br = new BufferedReader(isr); - char [] buf = new char[1024]; + char[] buf = new char[1024]; int len = 0; + // 循环读取文件内容,直到读取结束 while ((len = br.read(buf)) > 0) { sb.append(buf, 0, len); } } else { + // 如果无法打开raw资源文件,记录错误日志并退出方法 Log.e(TAG, "Read introduction file error"); return; } } catch (IOException e) { + // 捕获IOException异常,记录堆栈信息并退出方法 e.printStackTrace(); return; } finally { - if(in != null) { + // 确保关闭InputStream资源,避免资源泄露 + if (in != null) { try { in.close(); } catch (IOException e) { @@ -190,13 +211,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 创建一个空的工作便笺,用于存储应用介绍文本 WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.RED); + // 设置工作便笺的文本内容为刚才读取的介绍文本 note.setWorkingText(sb.toString()); + // 尝试保存便笺,如果保存成功,更新SharedPreferences标记,表示已经添加过介绍 if (note.saveNote()) { sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); } else { + // 如果保存便笺失败,记录错误日志并退出方法 Log.e(TAG, "Save introduction note error"); return; } @@ -236,50 +261,78 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private ActionMode mActionMode; private MenuItem mMoveMenu; + /** + * 创建操作模式并设置相关操作和视图 + * 在此方法中,我们会根据当前的笔记状态设置不同的操作选项,并且根据用户的选择做出响应 + * @param mode 当前的操作模式 + * @param menu 菜单对象,用于充气和设置菜单项的可见性以及监听器 + * @return 返回true表示处理了这个事件 + */ public boolean onCreateActionMode(ActionMode mode, Menu 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); } + // 设置当前操作模式 mActionMode = mode; + // 设置笔记列表适配器的选择模式为true,允许项目选择 mNotesListAdapter.setChoiceMode(true); + // 禁用长按可点击,防止弹出上下文菜单 mNotesListView.setLongClickable(false); + // 隐藏添加新笔记的按钮,因为当前在操作模式下 mAddNewNote.setVisibility(View.GONE); - View customView = LayoutInflater.from(NotesListActivity.this).inflate( - R.layout.note_list_dropdown_menu, null); + // 创建自定义视图并充气布局,用于操作模式的自定义视图 + 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(){ + // 设置下拉菜单项的点击监听器,用于全选和取消全选笔记 + 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(); - // Update dropdown menu + // 更新下拉菜单标题,使用资源文件中的格式,并填入已选择的笔记数量 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); } @@ -312,53 +365,76 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt updateMenu(); } + /** + * 处理菜单项点击事件 + * + * @param item 被点击的菜单项 + * @return 如果菜单项被处理,则返回true;否则返回false + */ 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())); + mNotesListAdapter.getSelectedCount())); builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - batchDelete(); - } - }); + 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; default: + // 如果菜单项ID不匹配任何已知选项,则返回false return false; } + // 如果处理了菜单项,则返回true return true; } } private class NewNoteOnTouchListener implements OnTouchListener { + /** + * 处理触摸事件,用于处理“新建便笺”按钮的透明部分触摸事件 + * 当用户触摸屏幕时,计算触摸位置并决定是否将事件分发到位于按钮后的列表视图 + * + * @param v 被触摸的视图 + * @param event 触摸事件 + * @return 如果事件被处理并消耗,则返回true;否则返回false + */ 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(); @@ -372,45 +448,65 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * Notice that, if the background of the button changes, the formula should * also change. This is very bad, just for the UI designer's strong requirement. */ + + // 处理"新建便笺"按钮透明部分的触摸事件 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); + // 标记为需要分发事件 mDispatch = true; + // 分发触摸事件到列表视图并返回结果 return mNotesListView.dispatchTouchEvent(event); } } break; } case MotionEvent.ACTION_MOVE: { + // 如果事件需要分发,则更新分发Y坐标并重新设置事件位置 if (mDispatch) { mDispatchY += (int) event.getY() - mOriginY; event.setLocation(event.getX(), mDispatchY); + // 分发触摸事件到列表视图并返回结果 return mNotesListView.dispatchTouchEvent(event); } break; } default: { + // 在触摸事件结束时,重置分发标记并分发最后一个事件到列表视图 if (mDispatch) { event.setLocation(event.getX(), mDispatchY); mDispatch = false; + // 分发触摸事件到列表视图并返回结果 return mNotesListView.dispatchTouchEvent(event); } break; } } + // 如果事件没有被处理,则返回false return false; } }; + /** + * 异步启动笔记列表查询 + * 根据当前文件夹ID配置查询条件,然后启动背景查询处理程序执行查询 + * 查询笔记内容,排序依据笔记类型和修改日期降序排列 + */ private void startAsyncNotesListQuery() { + // 根据当前文件夹ID选择适当的查询条件 String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + // 启动背景查询处理程序执行笔记列表查询 + // 参数包括查询标识符、外部上下文、查询的URI、投影数组、选择条件和参数以及排序顺序 mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { String.valueOf(mCurrentFolderId) @@ -422,43 +518,77 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt super(contentResolver); } + /** + * 当数据库查询完成时调用的方法 + * + * @param token 查询的标识符,用于区分不同的查询任务 + * @param cookie 查询操作的附加数据,在此例程中未使用 + * @param cursor 查询结果的游标对象,包含查询到的数据 + */ @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // 根据不同的token值执行相应的处理 switch (token) { + // 当token为获取笔记列表的查询标识时 case FOLDER_NOTE_LIST_QUERY_TOKEN: + // 更新笔记列表的适配器,以显示新的查询结果 mNotesListAdapter.changeCursor(cursor); break; + // 当token为获取文件夹列表的查询标识时 case FOLDER_LIST_QUERY_TOKEN: + // 检查查询结果是否有效且包含数据 if (cursor != null && cursor.getCount() > 0) { + // 显示文件夹列表菜单,传递查询结果作为参数 showFolderListMenu(cursor); } else { + // 记录查询失败的日志信息 Log.e(TAG, "Query folder failed"); } break; + // 对于其他token值,不做任何处理 default: return; } } } + /** + * 显示文件夹列表菜单 + * + * @param cursor 游标对象,用于填充文件夹适配器的数据 + */ 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() { + /** + * 当对话框中的项被点击时调用 + * + * @param dialog 被点击的对话框 + * @param which 被点击的对话框项的索引 + */ 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(); } @@ -469,30 +599,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } + /** + * 异步删除选中的便签或便签小部件 + * 此方法根据当前模式(同步模式或非同步模式)决定便签的处理方式: + * 在非同步模式下直接删除便签,在同步模式下则将便签移动到回收站文件夹 + */ private void batchDelete() { + // 创建一个异步任务,用于在后台进行删除操作 new AsyncTask>() { + /** + * 在后台线程中执行删除或移动操作 + * @param unused 未使用参数 + * @return 返回涉及的便签小部件集合 + */ protected HashSet doInBackground(Void... unused) { + // 获取当前选中的便签小部件 HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 根据是否为同步模式决定处理方式 if (!isSyncMode()) { - // if not synced, delete notes directly + // 如果不是同步模式,直接删除便签 if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter .getSelectedItemIds())) { + // 删除成功,不需额外处理 } else { + // 删除失败,记录错误日志 Log.e(TAG, "Delete notes error, should not happens"); } } else { - // in sync mode, we'll move the deleted note into the trash - // folder + // 如果是同步模式,将删除的便签移动到回收站文件夹 if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + // 移动失败,记录错误日志 Log.e(TAG, "Move notes to trash folder error, should not happens"); } } + // 返回涉及的便签小部件集合,用于后续更新小部件视图 return widgets; } + /** + * 在主线程中更新便签小部件视图并结束操作模式 + * @param widgets 便签小部件集合 + */ @Override protected void onPostExecute(HashSet widgets) { + // 遍历每个便签小部件,更新其视图 if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -501,28 +652,44 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } } + // 结束操作模式 mModeCallBack.finishActionMode(); } }.execute(); } + /** + * 删除指定的文件夹 + * 如果文件夹是根文件夹,则记录错误并返回 + * 在非同步模式下,直接删除文件夹;在同步模式下,将删除的文件夹移动到回收站 + * 如果文件夹关联了小部件,则更新小部件 + * + * @param folderId 要删除的文件夹的ID + */ private void deleteFolder(long folderId) { + // 检查是否尝试删除根文件夹,记录错误并退出 if (folderId == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Wrong folder id, should not happen " + folderId); return; } + // 使用HashSet存储待删除的文件夹ID HashSet ids = new HashSet(); ids.add(folderId); - HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, - folderId); + + // 获取与文件夹关联的小部件集合 + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); + + // 根据同步模式选择删除方式 if (!isSyncMode()) { - // if not synced, delete folder directly + // 如果不是同步模式,直接删除文件夹 DataUtils.batchDeleteNotes(mContentResolver, ids); } else { - // in sync mode, we'll move the deleted folder into the trash folder + // 如果是同步模式,将删除的文件夹移动到回收站 DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); } + + // 如果存在关联的小部件,遍历并更新小部件 if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -540,20 +707,36 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } + /** + * 打开指定的文件夹,并设置UI状态 + * + * @param data 要打开的文件夹的NoteItemData对象,包含文件夹的相关信息 + */ private void openFolder(NoteItemData data) { + // 设置当前文件夹ID mCurrentFolderId = data.getId(); + // 启动异步查询以获取笔记列表 startAsyncNotesListQuery(); + + // 如果打开的是通话记录文件夹 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 设置状态为通话记录文件夹状态 mState = ListEditState.CALL_RECORD_FOLDER; + // 隐藏添加新笔记按钮,因为通话记录文件夹可能不支持添加新笔记 mAddNewNote.setVisibility(View.GONE); } else { + // 否则,设置状态为子文件夹状态 mState = ListEditState.SUB_FOLDER; } + + // 设置标题栏文本,如果是通话记录文件夹,则使用资源文件中的名称 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); } @@ -579,43 +762,70 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } + /** + * 显示创建或修改文件夹的对话框 + * + * @param create 如果为true,则显示创建文件夹的对话框;如果为false,则显示修改文件夹名称的对话框 + */ private void showCreateOrModifyFolderDialog(final boolean create) { + // 使用AlertDialog.Builder创建对话框 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(); @@ -627,22 +837,23 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt 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 @@ -650,6 +861,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public void onTextChanged(CharSequence s, int start, int before, int count) { + // 根据输入文本的长度来启用或禁用“确定”按钮 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } else { @@ -665,45 +877,72 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } @Override + /** + * 处理后退按钮按下事件 + * 根据当前的状态不同,执行不同的后退逻辑 + */ public void onBackPressed() { + // 根据当前状态选择相应的后退行为 switch (mState) { case SUB_FOLDER: - mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mState = ListEditState.NOTE_LIST; - startAsyncNotesListQuery(); - mTitleBar.setVisibility(View.GONE); + // 当前在子文件夹中,返回上级文件夹 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 设置当前文件夹ID为根文件夹ID + mState = ListEditState.NOTE_LIST; // 更改状态为笔记列表 + startAsyncNotesListQuery(); // 开始异步查询笔记列表 + mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 break; case CALL_RECORD_FOLDER: - mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mState = ListEditState.NOTE_LIST; - mAddNewNote.setVisibility(View.VISIBLE); - mTitleBar.setVisibility(View.GONE); - startAsyncNotesListQuery(); + // 当前在通话录音文件夹中,返回笔记列表 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 设置当前文件夹ID为根文件夹ID + mState = ListEditState.NOTE_LIST; // 更改状态为笔记列表 + mAddNewNote.setVisibility(View.VISIBLE); // 显示添加新笔记按钮 + mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 + startAsyncNotesListQuery(); // 开始异步查询笔记列表 break; case NOTE_LIST: + // 当前在笔记列表中,调用默认的后退行为 super.onBackPressed(); break; default: + // 默认情况,不执行任何操作 break; } } + /** + * 根据小部件类型更新指定小部件的配置和显示 + * + * @param appWidgetId 小部件的唯一标识符 + * @param appWidgetType 小部件的类型,决定如何更新 + * + * 本方法通过发送广播来请求更新特定类型的小部件它首先根据小部件类型选择正确的 + * 小部件提供者类,然后构造一个意图并指定小部件的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); } @@ -728,15 +967,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt @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); @@ -744,6 +987,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + // 确认删除当前焦点的数据项对应的文件夹 deleteFolder(mFocusNoteDataItem.getId()); } }); @@ -751,67 +995,94 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.show(); break; case MENU_FOLDER_CHANGE_NAME: + // 显示修改文件夹名称的对话框 showCreateOrModifyFolderDialog(false); break; default: + // 其他情况不做操作 break; } return true; } + /** + * 在选项菜单显示前进行准备。 + * 根据当前状态设置不同的菜单项。 + * + * @param menu 菜单对象 + * @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); } + return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { + // 根据选中的菜单项执行相应的逻辑 switch (item.getItemId()) { 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 { + // 进入设置页面 startPreferenceActivity(); } break; } case R.id.menu_setting: { + // 进入设置页面 startPreferenceActivity(); break; } case R.id.menu_new_note: { + // 创建新的便签 createNewNote(); break; } - case R.id.menu_search: + case R.id.menu_search: { + // 处理搜索请求 onSearchRequested(); break; + } default: break; } @@ -824,17 +1095,35 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 将便签导出为文本文件 + * 该方法使用BackupUtils类的功能将便签数据导出为文本文件 + * 导出操作在后台线程中执行,以避免阻塞主线程 + */ private void exportNoteToText() { + // 获取BackupUtils的实例,用于执行备份操作 final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 使用AsyncTask在后台执行导出操作 new AsyncTask() { + /** + * 在后台线程中执行的耗时操作 + * 此方法调用BackupUtils的exportToText方法进行文本文件的导出 + */ @Override protected Integer doInBackground(Void... unused) { return backup.exportToText(); } + /** + * 在后台操作完成后执行的操作 + * 根据导出结果展示不同的对话框通知用户 + * + * @param result 导出操作的结果,可能的值包括STATE_SD_CARD_UNMOUONTED、STATE_SUCCESS、STATE_SYSTEM_ERROR + */ @Override protected void onPostExecute(Integer result) { + // SD卡未挂载时的处理 if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(NotesListActivity.this @@ -843,7 +1132,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt .getString(R.string.error_sdcard_unmounted)); builder.setPositiveButton(android.R.string.ok, null); builder.show(); - } else if (result == BackupUtils.STATE_SUCCESS) { + } // 导出成功时的处理 + else if (result == BackupUtils.STATE_SUCCESS) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(NotesListActivity.this .getString(R.string.success_sdcard_export)); @@ -852,7 +1142,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt .getExportedTextFileName(), backup.getExportedTextFileDir())); builder.setPositiveButton(android.R.string.ok, null); builder.show(); - } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + } // 系统错误时的处理 + 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)); @@ -917,11 +1208,20 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } + /** + * 启动查询目标文件夹的操作 + * + * 此方法用于构建查询条件并启动一个查询任务,该任务将根据当前的应用状态或编辑状态 + * 查询相关的文件夹列表。查询结果将用于展示或编辑笔记。 + */ private void startQueryDestinationFolders() { + // 定义查询条件的SQL语句模板 String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.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, @@ -935,20 +1235,41 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt NoteColumns.MODIFIED_DATE + " DESC"); } + /** + * 处理列表项长按事件的方法 + * + * 当用户在列表项上长按时,此方法会被调用它根据当前焦点的笔记类型(单个笔记或文件夹) + * 来决定是启动选择模式还是准备处理文件夹的上下文菜单 + * + * @param parent 触发事件的父适配器 + * @param view 被长按的视图 + * @param position 被长按视图在列表中的位置 + * @param id 被长按视图的行ID + * @return 是否拦截事件,这里总是返回false表示不拦截 + */ public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + // 检查当前视图是否为NotesListItem类型 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); } } + // 不拦截事件,允许其他监听器处理 return false; } } diff --git a/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/main/java/net/micode/notes/ui/NotesListItem.java index 1221e80..994263f 100644 --- a/src/main/java/net/micode/notes/ui/NotesListItem.java +++ b/src/main/java/net/micode/notes/ui/NotesListItem.java @@ -48,15 +48,31 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } + /** + * 绑定数据到视图项 + * + * 此方法负责根据提供的数据更新视图,包括显示/隐藏复选框、设置标题文本、设置警告图标等 + * 它处理不同类型的笔记数据(如普通笔记、文件夹、通话记录等)并根据数据的类型和状态更新视图的显示 + * + * @param context 上下文,用于访问资源 + * @param data 要绑定的笔记数据项 + * @param choiceMode 是否处于选择模式 + * @param checked 是否选中复选框,仅在选择模式下且数据为笔记时有效 + */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 在选择模式下,且数据为笔记时,显示复选框并设置选中状态 if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); mCheckBox.setChecked(checked); } else { + // 否则,隐藏复选框 mCheckBox.setVisibility(View.GONE); } + // 存储当前项的数据 mItemData = data; + + // 如果数据ID为通话记录文件夹ID,则设置相应的图标和文本 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); @@ -65,6 +81,7 @@ public class NotesListItem extends LinearLayout { + context.getString(R.string.format_folder_files_count, data.getNotesCount())); mAlert.setImageResource(R.drawable.call_record); } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 如果数据的父ID为通话记录文件夹ID,则设置相应的文本和图标 mCallName.setVisibility(View.VISIBLE); mCallName.setText(data.getCallName()); mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); @@ -76,13 +93,12 @@ public class NotesListItem extends LinearLayout { mAlert.setVisibility(View.GONE); } } else { + // 对于其他类型的数据,根据其类型设置相应的文本和图标 mCallName.setVisibility(View.GONE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); - if (data.getType() == Notes.TYPE_FOLDER) { mTitle.setText(data.getSnippet() - + context.getString(R.string.format_folder_files_count, - data.getNotesCount())); + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); @@ -94,14 +110,28 @@ public class NotesListItem extends LinearLayout { } } } + + // 设置时间文本为相对于修改时间的相对时间跨度 mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + // 根据数据设置项的背景 setBackground(data); } + /** + * 根据NoteItemData对象设置当前视图的背景 + * 该方法通过判断笔记项的类型(如单个笔记、最后一个笔记等状态)以及背景颜色ID来设置适当的背景资源 + * 对于不同状态的笔记项,会调用不同的背景资源方法,确保视觉上的一致性和辨识度 + * + * @param data NoteItemData对象,包含笔记项的信息,如类型、是否是单个/最后一个/第一个笔记等 + */ private void setBackground(NoteItemData data) { + // 获取笔记项的背景颜色资源ID int id = data.getBgColorId(); + + // 判断笔记项的类型 if (data.getType() == Notes.TYPE_NOTE) { + // 如果是笔记类型,根据笔记的状态选择背景资源 if (data.isSingle() || data.isOneFollowingFolder()) { setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); } else if (data.isLast()) { @@ -112,6 +142,7 @@ public class NotesListItem extends LinearLayout { setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } } else { + // 如果不是笔记类型(即文件夹类型),设置默认的文件夹背景资源 setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } }