From 9a7fa2bab84b2004a5c6c6e77af463d67a6b8ccc Mon Sep 17 00:00:00 2001 From: zx <2396494751@qq.com> Date: Tue, 23 Dec 2025 21:04:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../net/micode/notes/tool/BackupUtils.java | 258 ++++++++++--- .../src/net/micode/notes/tool/DataUtils.java | 146 +++++-- .../micode/notes/tool/GTaskStringUtils.java | 61 ++- .../net/micode/notes/tool/ResourceParser.java | 107 ++++- .../micode/notes/ui/AlarmAlertActivity.java | 154 ++++++-- .../micode/notes/ui/AlarmInitReceiver.java | 75 +++- .../net/micode/notes/ui/AlarmReceiver.java | 23 +- .../net/micode/notes/ui/DateTimePicker.java | 364 +++++++++++++----- .../micode/notes/ui/DateTimePickerDialog.java | 79 +++- .../src/net/micode/notes/ui/DropdownMenu.java | 51 ++- .../micode/notes/ui/FoldersListAdapter.java | 86 ++++- .../src/net/micode/notes/ui/NoteEditText.java | 165 +++++++- .../src/net/micode/notes/ui/NoteItemData.java | 246 +++++++++--- .../net/micode/notes/ui/NotesListAdapter.java | 146 ++++++- .../net/micode/notes/ui/NotesListItem.java | 109 +++++- .../notes/ui/NotesPreferenceActivity.java | 228 ++++++++++- .../notes/widget/NoteWidgetProvider_2x.java | 40 +- .../notes/widget/NoteWidgetProvider_4x.java | 41 +- 18 files changed, 2029 insertions(+), 350 deletions(-) diff --git a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java index 39f6ec4..4734dec 100644 --- a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,33 +14,62 @@ * limitations under the License. */ +// 包声明:归属小米便签的工具模块,提供笔记备份(导出为文本文件)核心功能 package net.micode.notes.tool; +// 导入安卓上下文类:访问资源、ContentResolver import android.content.Context; +// 导入安卓数据库游标类:查询便签/便签数据的核心载体 import android.database.Cursor; +// 导入安卓外部存储类:判断SD卡挂载状态、获取SD卡根目录 import android.os.Environment; +// 导入安卓文本工具类:判空、字符串处理 import android.text.TextUtils; +// 导入安卓日期格式化类:格式化便签修改时间、导出文件名 import android.text.format.DateFormat; +// 导入安卓日志类:输出备份过程中的日志(调试/错误) import android.util.Log; +// 导入小米便签资源类:引用字符串(文件路径、格式模板、文件夹名称) import net.micode.notes.R; +// 导入便签数据常量类:定义ContentURI、字段、便签类型、特殊文件夹ID等 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; - +/** + * 便签备份工具类 + * 核心特性: + * 1. 单例模式:全局唯一实例,避免重复初始化资源; + * 2. 核心功能:将便签数据库中的所有有效便签(排除回收站)导出为SD卡上的文本文件; + * 3. 导出范围: + * - 所有文件夹(排除回收站)及文件夹下的便签; + * - 通话记录文件夹及其中的通话记录便签; + * - 根目录下的普通便签; + * 4. 导出格式:按文件夹分组,包含便签修改时间、内容(普通文本/通话记录详情); + * 5. 状态码机制:返回备份操作的结果(SD卡未挂载、文件创建失败、成功等)。 + */ public class BackupUtils { + // 日志标签:用于备份过程的日志输出 private static final String TAG = "BackupUtils"; - // Singleton stuff + + // 单例实例:全局唯一的BackupUtils对象 private static BackupUtils sInstance; + /** + * 获取单例实例(线程安全) + * 采用同步方法确保多线程环境下实例唯一,避免重复创建 + * @param context 应用上下文:用于初始化内部导出器、访问资源 + * @return BackupUtils全局唯一实例 + */ public static synchronized BackupUtils getInstance(Context context) { if (sInstance == null) { sInstance = new BackupUtils(context); @@ -49,43 +78,76 @@ public class BackupUtils { } /** - * Following states are signs to represents backup or restore - * status + * 备份操作状态码:标识导出过程的结果,供外部判断操作是否成功 */ - // Currently, the sdcard is not mounted + // SD卡未挂载(无法创建/写入导出文件) public static final int STATE_SD_CARD_UNMOUONTED = 0; - // The backup file not exist + // 备份文件不存在(仅恢复操作使用,当前导出逻辑未用到) public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; - // The data is not well formated, may be changed by other programs + // 数据格式错误(导出时未用到,预留恢复操作的状态码) public static final int STATE_DATA_DESTROIED = 2; - // Some run-time exception which causes restore or backup fails + // 系统错误(如文件创建失败、IO异常等运行时错误) public static final int STATE_SYSTEM_ERROR = 3; - // Backup or restore success + // 导出操作成功完成 public static final int STATE_SUCCESS = 4; + // 文本导出器实例:封装实际的文本导出逻辑,与工具类解耦 private TextExport mTextExport; + /** + * 私有构造方法:单例模式的核心,禁止外部直接实例化 + * @param context 应用上下文:传递给TextExport初始化资源 + */ private BackupUtils(Context context) { mTextExport = new TextExport(context); } + /** + * 检查外部存储(SD卡)是否可用 + * 仅当SD卡处于“已挂载”状态时,才允许执行导出操作 + * @return true=SD卡已挂载且可读写,false=不可用 + */ private static boolean externalStorageAvailable() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } + /** + * 对外暴露的导出方法:触发便签导出为文本文件的核心逻辑 + * @return 操作状态码:参考BackupUtils的STATE_XXX常量 + */ public int exportToText() { return mTextExport.exportToText(); } + /** + * 获取本次导出的文件名(导出成功后有效) + * @return 导出文件的名称(如notes_20251223.txt) + */ public String getExportedTextFileName() { return mTextExport.mFileName; } + /** + * 获取本次导出的文件目录(导出成功后有效) + * @return 导出文件所在的目录路径(如/sdcard/MiNotes/) + */ public String getExportedTextFileDir() { return mTextExport.mFileDirectory; } + /** + * 内部文本导出器类:封装所有导出相关的核心逻辑 + * 与外部BackupUtils解耦,专注处理“查询便签数据→格式化→写入文本文件”的完整流程 + */ private static class TextExport { + /** + * 便签查询投影数组:仅查询导出所需的核心字段,减少IO开销 + * 字段说明: + * - NoteColumns.ID:便签/文件夹唯一ID; + * - NoteColumns.MODIFIED_DATE:便签最后修改时间(用于导出展示); + * - NoteColumns.SNIPPET:便签摘要/文件夹名称; + * - NoteColumns.TYPE:便签类型(文件夹/普通便签/系统项)。 + */ private static final String[] NOTE_PROJECTION = { NoteColumns.ID, NoteColumns.MODIFIED_DATE, @@ -93,12 +155,20 @@ public class BackupUtils { NoteColumns.TYPE }; + // 便签投影数组列索引常量:简化Cursor取值,避免硬编码 private static final int NOTE_COLUMN_ID = 0; - private static final int NOTE_COLUMN_MODIFIED_DATE = 1; - private static final int NOTE_COLUMN_SNIPPET = 2; + /** + * 便签数据查询投影数组:查询便签的具体内容(普通文本/通话记录) + * 字段说明: + * - DataColumns.CONTENT:普通便签内容/通话记录附件位置; + * - DataColumns.MIME_TYPE:数据类型(普通便签/通话记录); + * - DataColumns.DATA1:通话记录时间; + * - DataColumns.DATA4:通话记录手机号; + * 其他DATA字段为预留,暂未使用。 + */ private static final String[] DATA_PROJECTION = { DataColumns.CONTENT, DataColumns.MIME_TYPE, @@ -108,148 +178,198 @@ public class BackupUtils { DataColumns.DATA4, }; + // 便签数据投影数组列索引常量 private static final int DATA_COLUMN_CONTENT = 0; - private static final int DATA_COLUMN_MIME_TYPE = 1; - private static final int DATA_COLUMN_CALL_DATE = 2; - private static final int DATA_COLUMN_PHONE_NUMBER = 4; + /** + * 导出文本格式模板数组:从资源文件读取,适配多语言,避免硬编码文本格式 + * 数组索引对应: + * - FORMAT_FOLDER_NAME:文件夹名称展示格式(如“【文件夹:XXX】”); + * - FORMAT_NOTE_DATE:便签修改时间展示格式(如“修改时间:2025-12-23 15:30”); + * - FORMAT_NOTE_CONTENT:便签内容展示格式(如“内容:XXX”)。 + */ private final String [] TEXT_FORMAT; + // 格式模板索引常量 private static final int FORMAT_FOLDER_NAME = 0; private static final int FORMAT_NOTE_DATE = 1; private static final int FORMAT_NOTE_CONTENT = 2; + // 上下文:用于访问ContentResolver、资源文件 private Context mContext; - private String mFileName; - private String mFileDirectory; + private String mFileName; // 导出文件的名称(如notes_20251223.txt) + private String mFileDirectory; // 导出文件的目录路径(如/sdcard/MiNotes/) + /** + * 文本导出器构造方法 + * @param context 应用上下文:读取格式模板、资源字符串 + */ public TextExport(Context context) { + // 从资源文件加载导出文本的格式模板 TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); mContext = context; + // 初始化文件名/目录为空,导出成功后赋值 mFileName = ""; mFileDirectory = ""; } + /** + * 获取指定索引的文本格式模板 + * @param id 格式模板索引(FORMAT_FOLDER_NAME/FORMAT_NOTE_DATE/FORMAT_NOTE_CONTENT) + * @return 格式模板字符串 + */ private String getFormat(int id) { return TEXT_FORMAT[id]; } /** - * Export the folder identified by folder id to text + * 导出指定文件夹下的所有便签到打印流 + * @param folderId 文件夹ID:要导出的文件夹唯一标识 + * @param ps 打印流:指向SD卡的导出文件,用于写入文本内容 */ 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); + NOTE_PROJECTION, + NoteColumns.PARENT_ID + "=?", // 查询条件:父文件夹ID匹配 + new String[] { folderId }, + null); if (notesCursor != null) { + // 遍历文件夹下的所有便签 if (notesCursor.moveToFirst()) { do { - // Print note's last modified date + // 1. 打印便签最后修改时间(按格式模板) ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( - mContext.getString(R.string.format_datetime_mdhm), + mContext.getString(R.string.format_datetime_mdhm), // 时间格式:月日时分 notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); - // Query data belong to this note + // 2. 导出单条便签的详细内容 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 打印流:指向SD卡的导出文件 */ 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); + DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", // 查询条件:便签ID匹配 + new String[] { noteId }, + null); if (dataCursor != null) { + // 遍历便签的所有数据项(单条便签可能包含多个数据项,如通话记录+附件) if (dataCursor.moveToFirst()) { do { String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + // 1. 处理通话记录类型便签 if (DataConstants.CALL_NOTE.equals(mimeType)) { - // Print phone number + // 获取通话记录核心信息 String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); String location = dataCursor.getString(DATA_COLUMN_CONTENT); + // 打印手机号(非空时) if (!TextUtils.isEmpty(phoneNumber)) { - ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), - phoneNumber)); + 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 + .format(mContext.getString(R.string.format_datetime_mdhm), callDate))); + // 打印通话记录附件位置(非空时) if (!TextUtils.isEmpty(location)) { - ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), - location)); + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location)); } - } else if (DataConstants.NOTE.equals(mimeType)) { + } + // 2. 处理普通文本便签 + 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)); + 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 + * 执行核心导出逻辑:将所有有效便签导出为SD卡上的文本文件 + * 导出流程: + * 1. 检查SD卡是否可用; + * 2. 创建导出文件的打印流; + * 3. 导出所有有效文件夹(排除回收站)+ 通话记录文件夹; + * 4. 导出根目录下的普通便签; + * 5. 关闭打印流,返回操作状态码。 + * @return 操作状态码:参考BackupUtils的STATE_XXX常量 */ public int exportToText() { + // 步骤1:检查SD卡是否挂载,未挂载直接返回对应状态码 if (!externalStorageAvailable()) { Log.d(TAG, "Media was not mounted"); return STATE_SD_CARD_UNMOUONTED; } + // 步骤2:创建导出文件的打印流,失败则返回系统错误 PrintStream ps = getExportToTextPrintStream(); if (ps == null) { Log.e(TAG, "get print stream error"); return STATE_SYSTEM_ERROR; } - // First export folder and its notes + + // 步骤3:导出所有有效文件夹(排除回收站)+ 通话记录文件夹 Cursor folderCursor = mContext.getContentResolver().query( Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, + // 查询条件: + // - 类型为文件夹 + 父文件夹不是回收站; + // - 或ID为通话记录文件夹(特殊系统文件夹) "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " - + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null); + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, + null, + null); if (folderCursor != null) { if (folderCursor.moveToFirst()) { do { - // Print folder's name + // 处理文件夹名称:通话记录文件夹使用固定名称,其他文件夹用摘要 String folderName = ""; if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { folderName = mContext.getString(R.string.call_record_folder_name); } else { folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); } + // 打印文件夹名称(非空时) if (!TextUtils.isEmpty(folderName)) { ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); } + // 导出该文件夹下的所有便签 String folderId = folderCursor.getString(NOTE_COLUMN_ID); exportFolderToText(folderId, ps); } while (folderCursor.moveToNext()); @@ -257,51 +377,66 @@ public class BackupUtils { folderCursor.close(); } - // Export notes in root's folder + // 步骤4:导出根目录下的普通便签(父文件夹ID为0) Cursor noteCursor = mContext.getContentResolver().query( Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, - NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID - + "=0", null, null); + // 查询条件:类型为普通便签 + 父文件夹为根目录 + 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()); } noteCursor.close(); } + + // 步骤5:关闭打印流,释放文件资源 ps.close(); + // 导出成功,返回成功状态码 return STATE_SUCCESS; } /** - * Get a print stream pointed to the file {@generateExportedTextFile} + * 创建指向SD卡导出文件的PrintStream + * 核心逻辑:调用generateFileMountedOnSDcard创建文件,再包装为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) { 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) { + // 文件未找到异常:打印堆栈,返回null e.printStackTrace(); return null; } catch (NullPointerException e) { + // 空指针异常:打印堆栈,返回null e.printStackTrace(); return null; } @@ -310,35 +445,48 @@ public class BackupUtils { } /** - * Generate the text file to store imported data + * 在SD卡上生成导出用的文本文件 + * 文件名规则:带当前日期(如notes_20251223.txt),目录为预设的便签备份目录 + * @param context 应用上下文:读取目录路径、文件名格式资源 + * @param filePathResId 目录路径资源ID:如R.string.file_path(/MiNotes/) + * @param fileNameFormatResId 文件名格式资源ID:如R.string.file_name_txt_format(notes_%s.txt) + * @return 生成的File对象(成功)/null(失败) */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { StringBuilder sb = new StringBuilder(); + // 1. 拼接SD卡根目录 + 预设目录路径(如/sdcard/MiNotes/) sb.append(Environment.getExternalStorageDirectory()); sb.append(context.getString(filePathResId)); File filedir = new File(sb.toString()); + + // 2. 拼接完整文件路径:目录 + 带日期的文件名(如/sdcard/MiNotes/notes_20251223.txt) sb.append(context.getString( fileNameFormatResId, + // 格式化文件名中的日期部分(年-月-日) DateFormat.format(context.getString(R.string.format_date_ymd), System.currentTimeMillis()))); File file = new File(sb.toString()); try { + // 3. 创建目录(不存在时) if (!filedir.exists()) { filedir.mkdir(); } + // 4. 创建文件(不存在时) if (!file.exists()) { file.createNewFile(); } + // 5. 返回创建成功的文件对象 return file; } catch (SecurityException e) { + // 权限异常(如无SD卡写入权限):打印堆栈,返回null e.printStackTrace(); } catch (IOException e) { + // IO异常(如创建文件失败):打印堆栈,返回null e.printStackTrace(); } + // 创建失败,返回null return null; } -} - - +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java index 2a14982..cb09442 100644 --- a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -34,9 +34,19 @@ import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import java.util.ArrayList; 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) { if (ids == null) { Log.d(TAG, "the ids is null"); @@ -47,18 +57,23 @@ public class DataUtils { return true; } + // 批量操作列表,用于执行事务性删除 ArrayList operationList = new ArrayList(); for (long id : ids) { + // 保护系统根文件夹不被删除 if(id == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Don't delete system folder root"); continue; } + // 构建删除操作,根据URI删除指定ID的笔记 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; @@ -72,14 +87,28 @@ public class DataUtils { return false; } + /** + * 移动单条笔记到目标文件夹 + * @param resolver ContentResolver用于数据库操作 + * @param id 笔记ID + * @param srcFolderId 源文件夹ID(记录原始位置) + * @param desFolderId 目标文件夹ID + */ public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { ContentValues values = new ContentValues(); - values.put(NoteColumns.PARENT_ID, desFolderId); - values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); - values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.PARENT_ID, desFolderId); // 设置新的父文件夹 + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹 + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改 resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); } + /** + * 批量移动笔记到指定文件夹 + * @param resolver ContentResolver用于数据库操作 + * @param ids 待移动的笔记ID集合 + * @param folderId 目标文件夹ID + * @return true成功,false失败 + */ public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { if (ids == null) { @@ -87,16 +116,19 @@ public class DataUtils { return true; } + // 批量操作列表,用于执行事务性更新 ArrayList operationList = new ArrayList(); for (long id : ids) { + // 构建更新操作,修改笔记的父文件夹 ContentProviderOperation.Builder builder = ContentProviderOperation .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); - builder.withValue(NoteColumns.PARENT_ID, folderId); - builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); + builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置新的父文件夹 + builder.withValue(NoteColumns.LOCAL_MODIFIED, 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()); @@ -112,9 +144,12 @@ public class DataUtils { } /** - * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + * 获取用户创建的文件夹数量(排除系统文件夹和回收站) + * @param resolver ContentResolver用于数据库操作 + * @return 文件夹数量 */ public static int getUserFolderCount(ContentResolver resolver) { + // 查询类型为文件夹且父ID不是回收站的记录数 Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, new String[] { "COUNT(*)" }, NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", @@ -125,18 +160,26 @@ public class DataUtils { if(cursor != null) { if(cursor.moveToFirst()) { try { - count = cursor.getInt(0); + count = cursor.getInt(0); // 获取第一列的计数结果 } catch (IndexOutOfBoundsException e) { Log.e(TAG, "get folder count failed:" + e.toString()); } finally { - cursor.close(); + cursor.close(); // 确保游标被关闭 } } } return count; } + /** + * 检查笔记是否在可见数据库中(不在回收站) + * @param resolver ContentResolver用于数据库操作 + * @param noteId 笔记ID + * @param type 笔记类型 + * @return true存在且可见,false不存在或已被删除到回收站 + */ public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + // 查询指定ID和类型的笔记,且父ID不是回收站 Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, @@ -145,7 +188,7 @@ public class DataUtils { boolean exist = false; if (cursor != null) { - if (cursor.getCount() > 0) { + if (cursor.getCount() > 0) { // 检查是否有匹配的记录 exist = true; } cursor.close(); @@ -153,13 +196,20 @@ public class DataUtils { 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); boolean exist = false; if (cursor != null) { - if (cursor.getCount() > 0) { + if (cursor.getCount() > 0) { // 检查是否有匹配的记录 exist = true; } cursor.close(); @@ -167,13 +217,20 @@ public class DataUtils { return exist; } + /** + * 检查数据记录是否存在 + * @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); boolean exist = false; if (cursor != null) { - if (cursor.getCount() > 0) { + if (cursor.getCount() > 0) { // 检查是否有匹配的记录 exist = true; } cursor.close(); @@ -181,7 +238,14 @@ public class DataUtils { return exist; } + /** + * 检查可见文件夹名称是否已存在(用于重命名或新建时的冲突检测) + * @param resolver ContentResolver用于数据库操作 + * @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 + @@ -189,7 +253,7 @@ public class DataUtils { new String[] { name }, null); boolean exist = false; if(cursor != null) { - if(cursor.getCount() > 0) { + if(cursor.getCount() > 0) { // 检查是否有同名文件夹 exist = true; } cursor.close(); @@ -197,7 +261,14 @@ public class DataUtils { return exist; } + /** + * 获取文件夹中所有笔记的桌面小部件信息 + * @param resolver ContentResolver用于数据库操作 + * @param folderId 文件夹ID + * @return 小部件属性集合 + */ public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + // 查询文件夹下所有笔记的小部件ID和类型 Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, NoteColumns.PARENT_ID + "=?", @@ -210,9 +281,10 @@ public class DataUtils { set = new HashSet(); do { try { + // 封装小部件属性对象 AppWidgetAttribute widget = new AppWidgetAttribute(); - widget.widgetId = c.getInt(0); - widget.widgetType = c.getInt(1); + widget.widgetId = c.getInt(0); // 小部件ID + widget.widgetType = c.getInt(1); // 小部件类型 set.add(widget); } catch (IndexOutOfBoundsException e) { Log.e(TAG, e.toString()); @@ -224,7 +296,14 @@ public class DataUtils { return set; } + /** + * 通过笔记ID获取通话号码(针对通话记录类型笔记) + * @param resolver ContentResolver用于数据库操作 + * @param noteId 笔记ID + * @return 通话号码,失败返回空字符串 + */ public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + // 查询指定笔记的通话号码,限定MIME类型为通话记录 Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.PHONE_NUMBER }, CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", @@ -233,17 +312,25 @@ public class DataUtils { if (cursor != null && cursor.moveToFirst()) { try { - return cursor.getString(0); + return cursor.getString(0); // 获取通话号码 } catch (IndexOutOfBoundsException e) { Log.e(TAG, "Get call number fails " + e.toString()); } finally { - cursor.close(); + cursor.close(); // 确保游标被关闭 } } return ""; } + /** + * 通过通话号码和日期查找对应的笔记ID + * @param resolver ContentResolver用于数据库操作 + * @param phoneNumber 通话号码 + * @param callDate 通话日期时间戳 + * @return 笔记ID,未找到返回0 + */ public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + // 使用自定义的PHONE_NUMBERS_EQUAL函数比较号码,确保同一联系人的不同格式号码能匹配 Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.NOTE_ID }, CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" @@ -254,7 +341,7 @@ public class DataUtils { if (cursor != null) { if (cursor.moveToFirst()) { try { - return cursor.getLong(0); + return cursor.getLong(0); // 获取笔记ID } catch (IndexOutOfBoundsException e) { Log.e(TAG, "Get call note id fails " + e.toString()); } @@ -264,7 +351,15 @@ public class DataUtils { return 0; } + /** + * 通过笔记ID获取摘要内容 + * @param resolver ContentResolver用于数据库操作 + * @param noteId 笔记ID + * @return 摘要内容 + * @throws IllegalArgumentException 笔记不存在时抛出异常 + */ public static String getSnippetById(ContentResolver resolver, long noteId) { + // 查询指定ID笔记的摘要字段 Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, new String [] { NoteColumns.SNIPPET }, NoteColumns.ID + "=?", @@ -274,7 +369,7 @@ public class DataUtils { if (cursor != null) { String snippet = ""; if (cursor.moveToFirst()) { - snippet = cursor.getString(0); + snippet = cursor.getString(0); // 获取摘要内容 } cursor.close(); return snippet; @@ -282,14 +377,19 @@ public class DataUtils { throw new IllegalArgumentException("Note is not found with id: " + noteId); } + /** + * 格式化笔记摘要(取第一行非空内容并去除首尾空格) + * @param snippet 原始摘要 + * @return 格式化后的摘要 + */ public static String getFormattedSnippet(String snippet) { if (snippet != null) { - snippet = snippet.trim(); + snippet = snippet.trim(); // 去除首尾空格 int index = snippet.indexOf('\n'); if (index != -1) { - snippet = snippet.substring(0, index); + snippet = snippet.substring(0, index); // 只保留第一行 } } return snippet; } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java index 666b729..0af3f5e 100644 --- a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java @@ -14,100 +14,159 @@ * limitations under the License. */ +// 包声明:归属小米便签的工具模块,定义Google Tasks(GTask)同步相关的核心字符串常量 package net.micode.notes.tool; +/** + * Google Tasks(GTask)同步字符串常量类 + * 核心职责: + * 1. 统一定义便签与GTask同步过程中JSON交互的所有字段名,避免硬编码; + * 2. 定义GTask侧的文件夹命名规则(区分MIUI便签专属文件夹、系统默认文件夹); + * 3. 定义同步元数据的标识字段(用于存储GTask与便签的映射关系); + * 设计目的:提升代码可维护性,便于统一修改GTask同步的字段/命名规则。 + */ public class GTaskStringUtils { + // ======================== GTask同步JSON交互 - 动作相关字段 ======================== + /** JSON字段:动作ID(标识单次同步操作的唯一ID) */ public final static String GTASK_JSON_ACTION_ID = "action_id"; + /** JSON字段:动作列表(批量同步时存储多个动作的数组) */ public final static String GTASK_JSON_ACTION_LIST = "action_list"; + /** JSON字段:动作类型(标识当前同步动作的类型,如创建/查询/移动/更新) */ public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + /** JSON字段:动作类型-创建(同步时向GTask创建新任务/文件夹) */ public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + /** JSON字段:动作类型-全量查询(从GTask拉取所有任务/文件夹数据) */ public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + /** JSON字段:动作类型-移动(将GTask任务/文件夹移动到指定目录) */ public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + /** JSON字段:动作类型-更新(更新GTask中已存在的任务/文件夹信息) */ public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + // ======================== GTask同步JSON交互 - 实体/用户相关字段 ======================== + /** JSON字段:创建者ID(标识GTask实体的创建者账号ID) */ public final static String GTASK_JSON_CREATOR_ID = "creator_id"; + /** JSON字段:子实体(存储GTask文件夹下的子任务/子文件夹) */ public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; + /** JSON字段:客户端版本(标识便签客户端的版本号,用于GTask服务端兼容) */ public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; + /** JSON字段:完成状态(标识GTask任务是否已完成,布尔值) */ public final static String GTASK_JSON_COMPLETED = "completed"; + /** JSON字段:当前列表ID(标识任务/文件夹所属的GTask列表ID) */ public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; + /** JSON字段:默认列表ID(GTask默认任务列表的ID) */ public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; + /** JSON字段:删除标记(标识GTask实体是否已被删除,布尔值) */ public final static String GTASK_JSON_DELETED = "deleted"; + /** JSON字段:目标列表(移动操作时的目标列表ID) */ public final static String GTASK_JSON_DEST_LIST = "dest_list"; + /** JSON字段:目标父节点(移动操作时的目标父实体ID) */ public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; + /** JSON字段:目标父节点类型(移动操作时目标父实体的类型:GROUP/TASK) */ public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; + /** JSON字段:实体增量(同步时仅传输实体的变更部分,减少数据传输) */ public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + /** JSON字段:实体类型(标识GTask实体的类型:任务/文件夹) */ public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + /** JSON字段:获取已删除项(同步时是否拉取GTask中已删除的实体) */ public final static String GTASK_JSON_GET_DELETED = "get_deleted"; + /** JSON字段:实体ID(GTask任务/文件夹的唯一标识ID) */ public final static String GTASK_JSON_ID = "id"; + /** JSON字段:索引(GTask实体在父节点中的排序索引) */ public final static String GTASK_JSON_INDEX = "index"; + /** JSON字段:最后修改时间(GTask实体的最后修改时间戳) */ public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + /** JSON字段:最新同步点(标识上次同步的位置,用于增量同步) */ public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; + /** JSON字段:列表ID(GTask列表的唯一标识ID) */ public final static String GTASK_JSON_LIST_ID = "list_id"; + /** JSON字段:列表数组(存储GTask所有列表的数组) */ public final static String GTASK_JSON_LISTS = "lists"; + /** JSON字段:名称(GTask任务/文件夹的名称) */ public final static String GTASK_JSON_NAME = "name"; + /** JSON字段:新ID(创建/移动操作后生成的新实体ID) */ public final static String GTASK_JSON_NEW_ID = "new_id"; + /** JSON字段:备注(GTask任务的备注内容,对应便签的正文) */ public final static String GTASK_JSON_NOTES = "notes"; + /** JSON字段:父节点ID(GTask实体的父文件夹/父任务ID) */ public final static String GTASK_JSON_PARENT_ID = "parent_id"; + /** JSON字段:前序兄弟ID(标识实体在父节点中的前一个兄弟实体ID,用于排序) */ public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + /** JSON字段:同步结果(GTask服务端返回的同步操作结果) */ public final static String GTASK_JSON_RESULTS = "results"; + /** JSON字段:源列表(移动操作时的源列表ID) */ public final static String GTASK_JSON_SOURCE_LIST = "source_list"; + /** JSON字段:任务数组(存储GTask所有任务的数组) */ public final static String GTASK_JSON_TASKS = "tasks"; + /** JSON字段:类型(兼容字段,同entity_type) */ public final static String GTASK_JSON_TYPE = "type"; + /** JSON字段:类型-分组(GTask文件夹的类型标识) */ public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + /** JSON字段:类型-任务(GTask普通任务的类型标识) */ public final static String GTASK_JSON_TYPE_TASK = "TASK"; + /** JSON字段:用户(标识GTask所属的用户账号信息) */ public final static String GTASK_JSON_USER = "user"; + // ======================== GTask侧文件夹命名规则 ======================== + /** MIUI便签专属文件夹前缀(区分GTask中其他文件夹,避免命名冲突) */ public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; + /** GTask侧默认文件夹名称(对应便签的根目录) */ public final static String FOLDER_DEFAULT = "Default"; + /** GTask侧通话记录文件夹名称(对应便签的通话记录文件夹) */ public final static String FOLDER_CALL_NOTE = "Call_Note"; + /** GTask侧元数据文件夹名称(存储便签与GTask的同步映射元数据) */ public final static String FOLDER_META = "METADATA"; + // ======================== 同步元数据标识字段 ======================== + /** 元数据头-GTask ID(存储便签对应的GTask实体ID) */ public final static String META_HEAD_GTASK_ID = "meta_gid"; + /** 元数据头-便签(存储便签核心信息的元数据标识) */ public final static String META_HEAD_NOTE = "meta_note"; + /** 元数据头-数据(存储便签扩展数据的元数据标识) */ public final static String META_HEAD_DATA = "meta_data"; + /** 元数据便签名称(GTask中元数据便签的固定名称,禁止修改/删除) */ public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java index 1ad3ad6..beca99b 100644 --- a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java +++ b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -22,24 +22,38 @@ import android.preference.PreferenceManager; import net.micode.notes.R; import net.micode.notes.ui.NotesPreferenceActivity; +/** + * 资源解析工具类 + * 负责管理笔记背景、字体大小、桌面小部件等资源的ID映射 + * 提供常量定义和静态方法获取各类资源 + */ 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, @@ -48,6 +62,7 @@ public class ResourceParser { R.drawable.edit_red }; + // 编辑界面标题栏背景资源数组 private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { R.drawable.edit_title_yellow, R.drawable.edit_title_blue, @@ -56,25 +71,49 @@ public class ResourceParser { R.drawable.edit_title_red }; + /** + * 获取笔记编辑界面背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ public static int getNoteBgResource(int id) { return BG_EDIT_RESOURCES[id]; } + /** + * 获取笔记标题栏背景资源ID + * @param id 颜色常量索引 + * @return 标题栏背景资源ID + */ public static int getNoteTitleBgResource(int id) { return BG_EDIT_TITLE_RESOURCES[id]; } } + /** + * 获取默认背景颜色ID + * 根据用户设置决定是随机颜色还是固定黄色 + * @param context 应用上下文 + * @return 背景颜色常量索引 + */ public static int getDefaultBgId(Context context) { + // 检查用户是否启用了随机背景色设置 if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + // 生成随机颜色索引 return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); } else { + // 返回默认黄色 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, @@ -83,6 +122,7 @@ public class ResourceParser { R.drawable.list_red_up }; + // 列表中间项背景资源数组 private final static int [] BG_NORMAL_RESOURCES = new int [] { R.drawable.list_yellow_middle, R.drawable.list_blue_middle, @@ -91,6 +131,7 @@ public class ResourceParser { R.drawable.list_red_middle }; + // 列表末项背景资源数组 private final static int [] BG_LAST_RESOURCES = new int [] { R.drawable.list_yellow_down, R.drawable.list_blue_down, @@ -99,6 +140,7 @@ public class ResourceParser { R.drawable.list_red_down, }; + // 列表单一项背景资源数组(只有一项时使用) private final static int [] BG_SINGLE_RESOURCES = new int [] { R.drawable.list_yellow_single, R.drawable.list_blue_single, @@ -107,28 +149,57 @@ public class ResourceParser { R.drawable.list_red_single }; + /** + * 获取列表首项背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ public static int getNoteBgFirstRes(int id) { return BG_FIRST_RESOURCES[id]; } + /** + * 获取列表末项背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ public static int getNoteBgLastRes(int id) { return BG_LAST_RESOURCES[id]; } + /** + * 获取列表单一项背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ public static int getNoteBgSingleRes(int id) { return BG_SINGLE_RESOURCES[id]; } + /** + * 获取列表中间项背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ public static int getNoteBgNormalRes(int id) { return BG_NORMAL_RESOURCES[id]; } + /** + * 获取文件夹背景资源ID(固定资源) + * @return 文件夹背景资源ID + */ public static int getFolderBgRes() { return R.drawable.list_folder; } } + /** + * 桌面小部件背景资源类 + * 管理2x2和4x4尺寸小部件的背景图片 + */ public static class WidgetBgResources { + // 2x2小部件背景资源数组 private final static int [] BG_2X_RESOURCES = new int [] { R.drawable.widget_2x_yellow, R.drawable.widget_2x_blue, @@ -137,10 +208,16 @@ public class ResourceParser { R.drawable.widget_2x_red, }; + /** + * 获取2x2小部件背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ 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, @@ -149,12 +226,22 @@ public class ResourceParser { R.drawable.widget_4x_red }; + /** + * 获取4x4小部件背景资源ID + * @param id 颜色常量索引 + * @return 背景资源ID + */ 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, @@ -162,20 +249,28 @@ public class ResourceParser { R.style.TextAppearanceSuper }; + /** + * 获取文本外观资源ID + * @param id 字体大小常量索引 + * @return 样式资源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} + * HACKME: 修复将资源ID存储在SharedPreference中的bug + * 存储的ID可能大于资源数组长度,此时返回默认字体大小 */ if (id >= TEXTAPPEARANCE_RESOURCES.length) { - return BG_DEFAULT_FONT_SIZE; + return BG_DEFAULT_FONT_SIZE; // 越界时返回默认值 } return TEXTAPPEARANCE_RESOURCES[id]; } + /** + * 获取可用资源数量 + * @return 字体大小选项总数 + */ public static int getResourcesSize() { return TEXTAPPEARANCE_RESOURCES.length; } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..1a09749 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -14,145 +14,245 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,作为便签提醒触发时的核心弹窗页面 package net.micode.notes.ui; +// 导入安卓Activity核心类:页面基础类 import android.app.Activity; +// 导入安卓对话框类:展示便签提醒内容和操作按钮 import android.app.AlertDialog; +// 导入安卓上下文类:访问系统服务、资源 import android.content.Context; +// 导入安卓对话框事件类:处理按钮点击、对话框关闭 import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnDismissListener; +// 导入安卓意图类:跳转便签编辑页、接收提醒所属便签ID import android.content.Intent; +// 导入安卓音频管理类:控制提醒铃声的音频流类型 import android.media.AudioManager; +// 导入安卓媒体播放类:播放提醒铃声(系统闹钟铃声) import android.media.MediaPlayer; +// 导入安卓铃声管理类:获取系统默认闹钟铃声 import android.media.RingtoneManager; +// 导入安卓URI类:标识铃声资源、便签ID import android.net.Uri; +// 导入安卓Bundle类:保存页面状态(此处未使用) import android.os.Bundle; +// 导入安卓电源管理类:判断屏幕是否亮屏,控制屏幕唤醒 import android.os.PowerManager; +// 导入安卓系统设置类:获取铃声静音模式配置 import android.provider.Settings; +// 导入安卓窗口相关类:设置窗口标记(唤醒屏幕、锁屏显示等) import android.view.Window; import android.view.WindowManager; +// 导入小米便签资源类:引用字符串、布局等资源 import net.micode.notes.R; +// 导入便签数据常量类:定义便签类型、ContentURI等 import net.micode.notes.data.Notes; +// 导入数据工具类:查询便签摘要、判断便签是否存在 import net.micode.notes.tool.DataUtils; +// 导入IO异常类:处理媒体播放器的IO错误 import java.io.IOException; - +/** + * 便签提醒弹窗页面 + * 核心职责: + * 1. 屏幕唤醒与显示控制: + * - 锁屏/熄屏时唤醒屏幕、保持屏幕常亮,确保提醒弹窗可见; + * - 亮屏时仅展示弹窗,不额外修改屏幕状态; + * 2. 提醒内容展示: + * - 从Intent中解析提醒所属便签ID,查询并裁剪便签摘要(最大60字符); + * - 弹窗展示便签摘要,提供“确认”“进入便签”按钮; + * 3. 提醒铃声播放: + * - 播放系统默认闹钟铃声,适配系统静音模式,铃声循环播放; + * - 对话框关闭时停止铃声并释放媒体资源; + * 4. 交互处理: + * - “确认”按钮:关闭弹窗,停止铃声; + * - “进入便签”按钮(仅亮屏时显示):跳转到便签编辑页; + * 关键说明: + * - 仅当便签仍存在于数据库中时,才展示弹窗和播放铃声; + * - 页面生命周期与对话框绑定,对话框关闭即结束页面。 + */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 提醒所属便签的ID:从Intent的Data字段解析,标识当前提醒对应的便签 private long mNoteId; + // 便签摘要文本:用于弹窗展示,最大长度限制为60字符 private String mSnippet; + // 便签摘要预览最大长度:超过该长度则裁剪并添加省略标记 private static final int SNIPPET_PREW_MAX_LEN = 60; + // 媒体播放器:用于播放系统闹钟铃声,循环播放提醒音 MediaPlayer mPlayer; + /** + * 页面创建核心方法:初始化屏幕状态、解析便签ID、播放铃声、展示弹窗 + * @param savedInstanceState 页面状态保存对象(此处未使用) + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // 隐藏页面标题栏:仅展示对话框,无需原生标题栏 requestWindowFeature(Window.FEATURE_NO_TITLE); + // 1. 窗口显示控制:确保弹窗在锁屏/熄屏时可见 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 - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕常亮 + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 唤醒屏幕 + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许屏幕锁定时显示 + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 适配装饰布局 } + // 2. 解析Intent,获取提醒所属便签ID并查询摘要 Intent intent = getIntent(); - try { + // 从Intent的Data字段解析便签ID(Data格式:content://notes/note/[id]) mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 根据便签ID查询摘要文本 mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 摘要长度裁剪:超过60字符则截取前60位并添加省略标记 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) { + // 解析ID失败(如格式错误):打印异常并结束页面 e.printStackTrace(); return; } + // 3. 初始化媒体播放器,判断便签是否存在并执行后续逻辑 mPlayer = new MediaPlayer(); + // 仅当便签仍存在于数据库中时,展示弹窗并播放铃声 if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { - showActionDialog(); - playAlarmSound(); + showActionDialog(); // 展示提醒弹窗 + playAlarmSound(); // 播放提醒铃声 } else { + // 便签已删除:直接结束页面,不展示任何内容 finish(); } } + /** + * 判断屏幕是否处于亮屏状态 + * @return boolean:true=亮屏,false=熄屏/锁屏 + */ private boolean isScreenOn() { + // 获取电源管理系统服务 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + // 返回屏幕亮屏状态 return pm.isScreenOn(); } + /** + * 播放系统默认闹钟铃声 + * 核心逻辑: + * - 获取系统默认闹钟铃声URI; + * - 适配系统静音模式,设置音频流类型; + * - 初始化媒体播放器,循环播放铃声。 + */ 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 { + // 默认使用闹钟音频流(STREAM_ALARM),优先级高于普通媒体流 mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); } + + // 初始化并启动媒体播放器 try { - mPlayer.setDataSource(this, url); - mPlayer.prepare(); - mPlayer.setLooping(true); - mPlayer.start(); + mPlayer.setDataSource(this, url); // 设置铃声数据源 + mPlayer.prepare(); // 准备播放器(同步) + mPlayer.setLooping(true); // 设置循环播放(持续提醒) + mPlayer.start(); // 启动播放 } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block + // 参数错误异常:打印堆栈,不中断流程 e.printStackTrace(); } catch (SecurityException e) { - // TODO Auto-generated catch block + // 权限异常(如铃声文件无访问权限):打印堆栈 e.printStackTrace(); } catch (IllegalStateException e) { - // TODO Auto-generated catch block + // 播放器状态异常(如重复prepare):打印堆栈 e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block + // IO异常(如铃声文件不存在):打印堆栈 e.printStackTrace(); } } + /** + * 展示便签提醒弹窗 + * 核心逻辑: + * - 标题为应用名称,内容为便签摘要; + * - 亮屏时显示“确认”+“进入便签”按钮,熄屏时仅显示“确认”按钮; + * - 绑定对话框关闭监听器,关闭时停止铃声并结束页面。 + */ private void showActionDialog() { AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle(R.string.app_name); - dialog.setMessage(mSnippet); - dialog.setPositiveButton(R.string.notealert_ok, this); + dialog.setTitle(R.string.app_name); // 弹窗标题:应用名称 + 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 点击的按钮类型(BUTTON_POSITIVE/ NEGATIVE) + */ public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_NEGATIVE: + // “进入便签”按钮:跳转到便签编辑页,传递便签ID Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, mNoteId); + intent.setAction(Intent.ACTION_VIEW); // 设置动作:查看/编辑便签 + intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID startActivity(intent); break; default: + // “确认”按钮:无额外操作(对话框关闭由OnDismissListener处理) break; } } + /** + * 对话框关闭事件处理:停止铃声,结束页面 + * @param dialog 关闭的对话框实例 + */ public void onDismiss(DialogInterface dialog) { - stopAlarmSound(); - finish(); + stopAlarmSound(); // 停止提醒铃声,释放媒体资源 + finish(); // 结束页面 } + /** + * 停止提醒铃声并释放媒体播放器资源 + * 核心逻辑:停止播放、释放播放器,避免资源泄漏 + */ private void stopAlarmSound() { if (mPlayer != null) { - mPlayer.stop(); - mPlayer.release(); - mPlayer = null; + mPlayer.stop(); // 停止播放 + mPlayer.release(); // 释放播放器资源 + mPlayer = null; // 置空引用,便于GC回收 } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java index f221202..0b3403d 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -14,52 +14,101 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,作为便签提醒的初始化广播接收器 package net.micode.notes.ui; +// 导入安卓闹钟管理类:用于注册/设置便签提醒闹钟 import android.app.AlarmManager; +// 导入安卓延迟意图类:封装广播Intent,供AlarmManager触发 import android.app.PendingIntent; +// 导入安卓广播接收器核心类:接收初始化广播(如开机/应用启动) import android.content.BroadcastReceiver; +// 导入安卓ContentURI工具类:拼接便签ID到Intent的Data字段,标识提醒所属便签 import android.content.ContentUris; +// 导入安卓上下文类:提供ContentResolver、系统服务等访问能力 import android.content.Context; +// 导入安卓意图类:创建指向AlarmReceiver的广播意图 import android.content.Intent; +// 导入安卓数据库游标类:查询未过期的便签提醒数据 import android.database.Cursor; +// 导入便签数据常量类:定义ContentURI、字段、便签类型等核心常量 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; - +/** + * 便签提醒初始化广播接收器 + * 核心职责: + * 1. 接收系统/应用的初始化广播(如设备开机、应用重启),重新初始化所有未过期的便签提醒; + * 2. 查询数据库中所有“提醒时间未过期”的普通便签,将其重新注册到AlarmManager; + * 3. 确保设备重启/应用重启后,未过期的便签提醒不会丢失,仍能按时触发; + * 关键说明: + * - 仅处理普通便签(TYPE_NOTE)的提醒,排除文件夹/系统项; + * - 使用AlarmManager.RTC_WAKEUP类型,确保提醒触发时唤醒设备,避免漏提醒。 + */ public class AlarmInitReceiver extends BroadcastReceiver { + /** + * 数据库查询投影数组:仅查询核心字段,减少IO开销 + * 字段说明: + * - NoteColumns.ID:便签唯一ID(用于标识提醒所属便签); + * - NoteColumns.ALERTED_DATE:便签的提醒时间戳(>0表示有提醒)。 + */ private static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE + NoteColumns.ID, // 0: 便签ID + NoteColumns.ALERTED_DATE // 1: 提醒时间戳 }; - private static final int COLUMN_ID = 0; - private static final int COLUMN_ALERTED_DATE = 1; + // 投影数组对应的列索引常量:简化Cursor取值,避免硬编码索引 + private static final int COLUMN_ID = 0; // 便签ID列索引 + private static final int COLUMN_ALERTED_DATE = 1; // 提醒时间戳列索引 + /** + * 广播接收核心方法:初始化未过期的便签提醒,重新注册到AlarmManager + * @param context 广播接收器上下文:用于访问ContentResolver、系统服务 + * @param intent 触发广播的Intent(如开机完成广播、应用启动广播) + */ @Override public void onReceive(Context context, Intent intent) { + // 1. 获取当前时间戳:作为筛选“未过期提醒”的阈值(仅处理提醒时间>当前时间的便签) long currentDate = System.currentTimeMillis(); + + // 2. 查询ContentResolver,获取所有未过期的普通便签提醒 + // 查询条件:提醒时间>当前时间 + 便签类型为普通便签(TYPE_NOTE) Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, - NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, - new String[] { String.valueOf(currentDate) }, - null); + PROJECTION, // 要查询的字段 + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件 + new String[] { String.valueOf(currentDate) }, // 条件参数(当前时间戳) + null); // 排序规则(默认) + // 3. 遍历Cursor,为每个未过期的提醒注册闹钟 if (c != null) { + // 游标移动到第一条数据(有未过期提醒时进入循环) if (c.moveToFirst()) { do { - long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 3.1 获取当前便签的提醒时间戳和ID + long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 提醒触发时间 + long noteId = c.getLong(COLUMN_ID); // 便签唯一ID + + // 3.2 创建广播Intent:指向AlarmReceiver(提醒触发时的处理接收器) Intent sender = new Intent(context, AlarmReceiver.class); - sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 将便签ID附加到Intent的Data字段:标识该提醒所属的便签,便于AlarmReceiver识别 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId)); + + // 3.3 创建PendingIntent:封装广播Intent,供AlarmManager触发 + // 参数说明:context=上下文,requestCode=请求码(此处为0),intent=广播意图,flags=标记(默认) PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 3.4 获取AlarmManager系统服务,注册提醒闹钟 AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); + // 设置闹钟:RTC_WAKEUP(基于系统时间,触发时唤醒设备)、提醒时间、PendingIntent alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); + + } while (c.moveToNext()); // 遍历所有未过期的提醒 } + // 4. 关闭Cursor:释放数据库资源,避免内存泄漏 c.close(); } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..167c9e3 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java @@ -14,17 +14,38 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,作为便签提醒功能的核心广播接收器 package net.micode.notes.ui; +// 导入安卓广播接收器核心类:接收系统/应用发送的广播事件 import android.content.BroadcastReceiver; +// 导入安卓上下文类:提供应用运行环境(启动Activity) import android.content.Context; +// 导入安卓意图类:用于启动提醒页面,传递数据 import android.content.Intent; +/** + * 便签提醒广播接收器 + * 核心职责: + * 1. 接收便签设置的提醒闹钟广播(由AlarmManager发送); + * 2. 接收到广播后,启动提醒弹窗页面(AlarmAlertActivity),展示便签提醒内容; + * 关键说明: + * - 广播接收器中启动Activity必须添加FLAG_ACTIVITY_NEW_TASK标记(无任务栈上下文); + * 典型使用场景:便签设置的提醒时间到达时,触发该接收器,弹出提醒窗口。 + */ public class AlarmReceiver extends BroadcastReceiver { + /** + * 广播接收核心方法:处理提醒广播,启动提醒页面 + * @param context 广播接收器的上下文:用于启动Activity(需添加新任务标记) + * @param intent 触发广播的Intent:携带提醒相关数据(如便签ID、内容等) + */ @Override public void onReceive(Context context, Intent intent) { + // 1. 修改Intent的目标类为提醒弹窗页面(AlarmAlertActivity) intent.setClass(context, AlarmAlertActivity.class); + // 2. 添加新任务标记:BroadcastReceiver无Activity任务栈,必须添加该标记才能启动Activity intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 3. 启动提醒弹窗页面,展示便签提醒内容 context.startActivity(intent); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java index 496b0cd..51f5c77 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java @@ -14,99 +14,171 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,自定义日期时间选择控件核心类 package net.micode.notes.ui; +// 导入日期格式化工具类:处理星期/日期的文本展示 import java.text.DateFormatSymbols; +// 导入日历类:核心的日期时间管理,处理年/月/日/时/分的计算与联动 import java.util.Calendar; +// 导入资源类:引用布局文件(datetime_picker.xml) import net.micode.notes.R; - +// 导入安卓上下文类:创建控件、加载资源 import android.content.Context; +// 导入日期格式化工具:判断系统24小时制设置 import android.text.format.DateFormat; +// 导入视图相关类:布局容器、视图可见性控制 import android.view.View; import android.widget.FrameLayout; +// 导入数字选择器:核心的日期/时间选择UI组件 import android.widget.NumberPicker; +/** + * 自定义日期时间选择控件 + * 核心特性: + * 1. 布局组成:集成日期(近7天)、小时、分钟、上午/下午(AM/PM)四个NumberPicker; + * 2. 时间联动:处理时间选择的边界联动(如分钟59→0时小时+1、小时23→0时日期+1); + * 3. 制式适配:支持24小时制/12小时制切换,自动适配系统默认设置; + * 4. 回调通知:时间选择变化时触发回调,传递最新的年/月/日/时/分; + * 5. 状态控制:支持整体启用/禁用所有选择器,统一管理交互状态; + * 典型使用场景:便签提醒时间设置的DateTimePickerDialog中作为核心选择UI。 + */ public class DateTimePicker extends FrameLayout { + // ======================== 基础常量 - 控件默认状态 ======================== + /** 控件默认启用状态:初始为启用 */ private static final boolean DEFAULT_ENABLE_STATE = true; + // ======================== 常量 - 时间数值范围 ======================== + /** 半天的小时数:12小时制的核心数值(AM/PM分界) */ private static final int HOURS_IN_HALF_DAY = 12; + /** 全天的小时数:24小时制的核心数值 */ private static final int HOURS_IN_ALL_DAY = 24; + /** 一周的天数:日期选择器展示近7天 */ private static final int DAYS_IN_ALL_WEEK = 7; + + // ======================== 常量 - NumberPicker取值范围 ======================== + /** 日期选择器最小值:0(对应近7天的起始索引) */ private static final int DATE_SPINNER_MIN_VAL = 0; + /** 日期选择器最大值:6(对应近7天的结束索引,DAYS_IN_ALL_WEEK - 1) */ private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + + /** 24小时制-小时选择器最小值:0(凌晨0点) */ private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + /** 24小时制-小时选择器最大值:23(深夜23点) */ private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + + /** 12小时制-小时选择器最小值:1(上午/下午1点) */ private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + /** 12小时制-小时选择器最大值:12(上午/下午12点) */ private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + + /** 分钟选择器最小值:0(整点) */ private static final int MINUT_SPINNER_MIN_VAL = 0; + /** 分钟选择器最大值:59(整点前1分钟) */ private static final int MINUT_SPINNER_MAX_VAL = 59; + + /** 上午/下午选择器最小值:0(AM/上午) */ private static final int AMPM_SPINNER_MIN_VAL = 0; + /** 上午/下午选择器最大值:1(PM/下午) */ private static final int AMPM_SPINNER_MAX_VAL = 1; + // ======================== 成员变量 - UI组件 ======================== + /** 日期选择器:展示近7天的日期(格式如“12.23 星期二”) */ private final NumberPicker mDateSpinner; + /** 小时选择器:根据24/12小时制展示不同范围的小时数 */ private final NumberPicker mHourSpinner; + /** 分钟选择器:0~59的分钟数选择 */ private final NumberPicker mMinuteSpinner; + /** 上午/下午选择器:仅12小时制显示,0=AM/上午,1=PM/下午 */ private final NumberPicker mAmPmSpinner; - private Calendar mDate; + // ======================== 成员变量 - 日期时间状态 ======================== + /** 核心日历对象:存储当前选中的日期时间,处理所有时间计算/联动 */ + private Calendar mDate; + /** 日期选择器展示文本数组:存储近7天的格式化日期文本(如“12.23 星期二”) */ private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; - + /** 上午/下午标记:true=AM/上午,false=PM/下午(仅12小时制有效) */ private boolean mIsAm; - + /** 24小时制标记:true=24小时制,false=12小时制 */ private boolean mIs24HourView; - + /** 控件启用状态:true=启用,false=禁用所有选择器 */ private boolean mIsEnabled = DEFAULT_ENABLE_STATE; - + /** 初始化标记:true=控件正在初始化,避免初始化时触发不必要的回调 */ private boolean mInitialising; + // ======================== 成员变量 - 回调监听 ======================== + /** 日期时间变化回调监听器:外部实现,接收时间变化通知 */ private OnDateTimeChangedListener mOnDateTimeChangedListener; + // ======================== 成员变量 - NumberPicker值变化监听器 ======================== + /** 日期选择器值变化监听器:处理日期切换,更新日历对象并触发回调 */ private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 调整日历的天数(新值-旧值为天数偏移量) mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + // 更新日期选择器的展示文本 updateDateControl(); + // 触发日期时间变化回调 onDateTimeChanged(); } }; + /** 小时选择器值变化监听器:处理小时切换,包含边界联动(小时→日期)和12/24小时制适配 */ private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - boolean isDateChanged = false; + boolean isDateChanged = false; // 日期是否变化标记 Calendar cal = Calendar.getInstance(); + + // 12小时制下的小时边界处理 if (!mIs24HourView) { + // 场景1:下午11点→12点 → 日期+1 if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; - } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + } + // 场景2:上午12点→11点 → 日期-1 + else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } + + // 小时11→12 或 12→11 时,切换上午/下午标记 if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { mIsAm = !mIsAm; - updateAmPmControl(); + updateAmPmControl(); // 更新上午/下午选择器 } - } else { + } + // 24小时制下的小时边界处理 + else { + // 场景1:23点→0点 → 日期+1 if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; - } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + } + // 场景2:0点→23点 → 日期-1 + else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } } + + // 计算并设置最终的小时数(适配12/24小时制) int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); mDate.set(Calendar.HOUR_OF_DAY, newHour); + // 触发时间变化回调 onDateTimeChanged(); + + // 日期变化时,更新日历的年/月/日 if (isDateChanged) { setCurrentYear(cal.get(Calendar.YEAR)); setCurrentMonth(cal.get(Calendar.MONTH)); @@ -115,152 +187,218 @@ public class DateTimePicker extends FrameLayout { } }; + /** 分钟选择器值变化监听器:处理分钟切换,包含边界联动(分钟→小时→日期) */ private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { int minValue = mMinuteSpinner.getMinValue(); int maxValue = mMinuteSpinner.getMaxValue(); - int offset = 0; + int offset = 0; // 小时偏移量 + + // 场景1:59分→0分 → 小时+1 if (oldVal == maxValue && newVal == minValue) { offset += 1; - } else if (oldVal == minValue && newVal == maxValue) { + } + // 场景2:0分→59分 → 小时-1 + else if (oldVal == minValue && newVal == maxValue) { offset -= 1; } + + // 小时需要偏移时,调整日历并更新相关选择器 if (offset != 0) { - mDate.add(Calendar.HOUR_OF_DAY, offset); - mHourSpinner.setValue(getCurrentHour()); - updateDateControl(); + mDate.add(Calendar.HOUR_OF_DAY, offset); // 调整小时 + mHourSpinner.setValue(getCurrentHour()); // 更新小时选择器 + updateDateControl(); // 更新日期选择器(小时偏移可能导致日期变化) + + // 根据新小时数更新上午/下午标记 int newHour = getCurrentHourOfDay(); if (newHour >= HOURS_IN_HALF_DAY) { mIsAm = false; - updateAmPmControl(); } else { mIsAm = true; - updateAmPmControl(); } + updateAmPmControl(); // 更新上午/下午选择器 } + + // 设置最终的分钟数 mDate.set(Calendar.MINUTE, newVal); + // 触发时间变化回调 onDateTimeChanged(); } }; + /** 上午/下午选择器值变化监听器:切换上午/下午,调整小时数(±12小时) */ private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 切换上午/下午标记 mIsAm = !mIsAm; + // 调整小时数:上午→下午 +12小时,下午→上午 -12小时 if (mIsAm) { mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); } else { mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); } + // 更新上午/下午选择器展示 updateAmPmControl(); + // 触发时间变化回调 onDateTimeChanged(); } }; + // ======================== 回调接口 - 日期时间变化通知 ======================== + /** + * 日期时间变化回调接口 + * 由外部(如DateTimePickerDialog)实现,接收控件选中的最新日期时间 + */ public interface OnDateTimeChangedListener { + /** + * 日期时间变化回调方法 + * @param view 当前的DateTimePicker控件实例 + * @param year 选中的年份 + * @param month 选中的月份(Calendar.MONTH,0=1月,11=12月) + * @param dayOfMonth 选中的日期(当月的第几天) + * @param hourOfDay 选中的小时(24小时制,0~23) + * @param minute 选中的分钟(0~59) + */ void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); } + // ======================== 构造方法 ======================== + /** + * 构造方法1:默认初始化(使用当前系统时间,适配系统24小时制) + * @param context 应用上下文 + */ public DateTimePicker(Context context) { this(context, System.currentTimeMillis()); } + /** + * 构造方法2:指定初始时间,适配系统24小时制 + * @param context 应用上下文 + * @param date 初始时间戳(毫秒级) + */ public DateTimePicker(Context context, long date) { this(context, date, DateFormat.is24HourFormat(context)); } + /** + * 构造方法3:指定初始时间和24小时制状态,核心初始化逻辑 + * @param context 应用上下文 + * @param date 初始时间戳(毫秒级) + * @param is24HourView 是否使用24小时制 + */ public DateTimePicker(Context context, long date, boolean is24HourView) { super(context); - mDate = Calendar.getInstance(); - mInitialising = true; + mDate = Calendar.getInstance(); // 初始化核心日历对象 + mInitialising = true; // 标记进入初始化阶段(避免触发不必要的回调) + // 初始化上午/下午标记:根据当前小时数判断(≥12为下午) mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + + // 加载控件布局(datetime_picker.xml) inflate(context, R.layout.datetime_picker, this); + // 初始化日期选择器 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + // 初始化小时选择器 mHourSpinner = (NumberPicker) findViewById(R.id.hour); mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + + // 初始化分钟选择器 mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); - mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setLongPressUpdateInterval(100); // 长按快速调整的间隔(100ms) mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); - String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + // 初始化上午/下午选择器 + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); // 获取系统AM/PM文本(适配多语言) mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); - mAmPmSpinner.setDisplayedValues(stringsForAmPm); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); // 设置AM/PM展示文本 mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - // update controls to initial state + // 更新控件到初始状态 updateDateControl(); updateHourControl(); updateAmPmControl(); + // 设置24小时制状态 set24HourView(is24HourView); - // set to current time + // 设置初始时间 setCurrentDate(date); + // 设置控件启用状态 setEnabled(isEnabled()); - // set the content descriptions + // 初始化完成,取消标记 mInitialising = false; } + // ======================== 重写方法 - 控件启用/禁用 ======================== + /** + * 设置控件整体启用/禁用状态 + * @param enabled true=启用(所有选择器可交互),false=禁用(所有选择器不可交互) + */ @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { - return; + return; // 状态未变化,直接返回 } super.setEnabled(enabled); + // 同步所有选择器的启用状态 mDateSpinner.setEnabled(enabled); mMinuteSpinner.setEnabled(enabled); mHourSpinner.setEnabled(enabled); mAmPmSpinner.setEnabled(enabled); + // 更新启用状态标记 mIsEnabled = enabled; } + /** + * 获取控件整体启用状态 + * @return true=启用,false=禁用 + */ @Override public boolean isEnabled() { return mIsEnabled; } + // ======================== 公共方法 - 日期时间获取/设置 ======================== /** - * Get the current date in millis - * - * @return the current date in millis + * 获取当前选中的日期时间戳(毫秒级) + * @return 选中时间的毫秒数 */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } /** - * Set the current date - * - * @param date The current date in millis + * 设置当前选中的日期时间(通过时间戳) + * @param date 要设置的时间戳(毫秒级) */ public void setCurrentDate(long date) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(date); + // 解析时间戳为年/月/日/时/分,调用重载方法设置 setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); } /** - * Set the current date - * - * @param year The current year - * @param month The current month - * @param dayOfMonth The current dayOfMonth - * @param hourOfDay The current hourOfDay - * @param minute The current minute + * 设置当前选中的日期时间(通过年/月/日/时/分) + * @param year 年份 + * @param month 月份(Calendar.MONTH,0=1月) + * @param dayOfMonth 日期(当月的第几天) + * @param hourOfDay 小时(24小时制,0~23) + * @param minute 分钟(0~59) */ public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) { @@ -272,86 +410,88 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current year - * - * @return The current year + * 获取当前选中的年份 + * @return 年份(如2025) */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); } /** - * Set current year - * - * @param year The current year + * 设置当前选中的年份 + * @param year 要设置的年份(如2025) */ public void setCurrentYear(int year) { + // 初始化中或年份未变化时,直接返回 if (!mInitialising && year == getCurrentYear()) { return; } mDate.set(Calendar.YEAR, year); - updateDateControl(); - onDateTimeChanged(); + updateDateControl(); // 更新日期选择器展示 + onDateTimeChanged(); // 触发时间变化回调 } /** - * Get current month in the year - * - * @return The current month in the year + * 获取当前选中的月份 + * @return 月份(Calendar.MONTH,0=1月,11=12月) */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); } /** - * Set current month in the year - * - * @param month The month in the year + * 设置当前选中的月份 + * @param month 要设置的月份(Calendar.MONTH,0=1月) */ public void setCurrentMonth(int month) { + // 初始化中或月份未变化时,直接返回 if (!mInitialising && month == getCurrentMonth()) { return; } mDate.set(Calendar.MONTH, month); - updateDateControl(); - onDateTimeChanged(); + updateDateControl(); // 更新日期选择器展示 + onDateTimeChanged(); // 触发时间变化回调 } /** - * Get current day of the month - * - * @return The day of the month + * 获取当前选中的日期(当月的第几天) + * @return 日期(1~31) */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } /** - * Set current day of the month - * - * @param dayOfMonth The day of the month + * 设置当前选中的日期(当月的第几天) + * @param dayOfMonth 要设置的日期(1~31) */ public void setCurrentDay(int dayOfMonth) { + // 初始化中或日期未变化时,直接返回 if (!mInitialising && dayOfMonth == getCurrentDay()) { return; } mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); - updateDateControl(); - onDateTimeChanged(); + updateDateControl(); // 更新日期选择器展示 + onDateTimeChanged(); // 触发时间变化回调 } /** - * Get current hour in 24 hour mode, in the range (0~23) - * @return The current hour in 24 hour mode + * 获取当前选中的小时(24小时制,0~23) + * @return 小时数(0~23) */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } + /** + * 内部方法:获取适配当前制式的小时数(24/12小时制) + * @return 24小时制返回0~23,12小时制返回1~12 + */ private int getCurrentHour() { if (mIs24HourView){ - return getCurrentHourOfDay(); + return getCurrentHourOfDay(); // 24小时制直接返回 } else { + // 12小时制转换:0点→12点,13点→1点,依此类推 int hour = getCurrentHourOfDay(); if (hour > HOURS_IN_HALF_DAY) { return hour - HOURS_IN_HALF_DAY; @@ -362,124 +502,160 @@ public class DateTimePicker extends FrameLayout { } /** - * Set current hour in 24 hour mode, in the range (0~23) - * - * @param hourOfDay + * 设置当前选中的小时(24小时制,0~23) + * @param hourOfDay 要设置的小时数(0~23) */ public void setCurrentHour(int hourOfDay) { + // 初始化中或小时未变化时,直接返回 if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { return; } mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + + // 12小时制下,更新上午/下午标记和选择器 if (!mIs24HourView) { if (hourOfDay >= HOURS_IN_HALF_DAY) { - mIsAm = false; + mIsAm = false; // ≥12为下午 if (hourOfDay > HOURS_IN_HALF_DAY) { - hourOfDay -= HOURS_IN_HALF_DAY; + hourOfDay -= HOURS_IN_HALF_DAY; // 转换为12小时制(13→1,14→2...) } } else { - mIsAm = true; + mIsAm = true; // <12为上午 if (hourOfDay == 0) { - hourOfDay = HOURS_IN_HALF_DAY; + hourOfDay = HOURS_IN_HALF_DAY; // 0点→12点 } } - updateAmPmControl(); + updateAmPmControl(); // 更新上午/下午选择器 } + + // 设置小时选择器的值 mHourSpinner.setValue(hourOfDay); + // 触发时间变化回调 onDateTimeChanged(); } /** - * Get currentMinute - * - * @return The Current Minute + * 获取当前选中的分钟 + * @return 分钟数(0~59) */ public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); } /** - * Set current minute + * 设置当前选中的分钟 + * @param minute 要设置的分钟数(0~59) */ public void setCurrentMinute(int minute) { + // 初始化中或分钟未变化时,直接返回 if (!mInitialising && minute == getCurrentMinute()) { return; } - mMinuteSpinner.setValue(minute); - mDate.set(Calendar.MINUTE, minute); - onDateTimeChanged(); + mMinuteSpinner.setValue(minute); // 设置分钟选择器的值 + mDate.set(Calendar.MINUTE, minute); // 更新日历对象 + onDateTimeChanged(); // 触发时间变化回调 } + // ======================== 公共方法 - 24小时制适配 ======================== /** - * @return true if this is in 24 hour view else false. + * 判断当前是否为24小时制 + * @return true=24小时制,false=12小时制 */ public boolean is24HourView () { return mIs24HourView; } /** - * Set whether in 24 hour or AM/PM mode. - * - * @param is24HourView True for 24 hour mode. False for AM/PM mode. + * 设置24小时制/12小时制 + * @param is24HourView true=24小时制(隐藏AM/PM选择器),false=12小时制(显示AM/PM选择器) */ public void set24HourView(boolean is24HourView) { if (mIs24HourView == is24HourView) { - return; + return; // 状态未变化,直接返回 } mIs24HourView = is24HourView; + // 显示/隐藏上午/下午选择器 mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + // 获取当前小时数(用于重置选择器) int hour = getCurrentHourOfDay(); + // 更新小时选择器的取值范围 updateHourControl(); + // 重新设置小时数(适配新的制式) setCurrentHour(hour); + // 更新上午/下午选择器状态 updateAmPmControl(); } + // ======================== 内部方法 - 控件更新 ======================== + /** + * 更新日期选择器的展示文本:生成近7天的格式化日期(如“12.23 星期二”) + */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(mDate.getTimeInMillis()); + // 计算近7天的起始日期(当前日期 - 4天,确保中间为当前日期) cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); - mDateSpinner.setDisplayedValues(null); + + mDateSpinner.setDisplayedValues(null); // 清空原有展示文本 + // 生成近7天的格式化日期文本 for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { cal.add(Calendar.DAY_OF_YEAR, 1); + // 格式化:月.日 星期(如“12.23 星期二”) mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); } + // 设置日期选择器的展示文本 mDateSpinner.setDisplayedValues(mDateDisplayValues); + // 设置默认选中中间项(当前日期) mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); - mDateSpinner.invalidate(); + mDateSpinner.invalidate(); // 刷新选择器UI } + /** + * 更新上午/下午选择器的状态:设置选中项、控制可见性 + */ private void updateAmPmControl() { if (mIs24HourView) { - mAmPmSpinner.setVisibility(View.GONE); + mAmPmSpinner.setVisibility(View.GONE); // 24小时制隐藏 } else { + // 设置选中项:0=AM/上午,1=PM/下午 int index = mIsAm ? Calendar.AM : Calendar.PM; mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); + mAmPmSpinner.setVisibility(View.VISIBLE); // 12小时制显示 } } + /** + * 更新小时选择器的取值范围:适配24/12小时制 + */ private void updateHourControl() { if (mIs24HourView) { + // 24小时制:0~23 mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); } else { + // 12小时制:1~12 mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); } } + // ======================== 公共方法 - 回调设置 ======================== /** - * Set the callback that indicates the 'Set' button has been pressed. - * @param callback the callback, if null will do nothing + * 设置日期时间变化回调监听器 + * @param callback 外部实现的监听器(null则不触发回调) */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { mOnDateTimeChangedListener = callback; } + // ======================== 内部方法 - 回调触发 ======================== + /** + * 触发日期时间变化回调:传递最新的年/月/日/时/分 + */ private void onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..6cc7119 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -14,74 +14,143 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,封装日期时间选择对话框,集成DateTimePicker控件 package net.micode.notes.ui; +// 导入日历类:管理选中的日期时间,处理年/月/日/时/分的设置 import java.util.Calendar; +// 导入小米便签资源类:引用对话框按钮文本、布局等资源 import net.micode.notes.R; +// 导入自定义日期时间选择控件:核心的日期时间选择UI import net.micode.notes.ui.DateTimePicker; import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; +// 导入安卓对话框相关类:基础AlertDialog、对话框点击事件 import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; +// 导入安卓日期格式化工具类:判断24小时制、格式化日期时间文本 import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * 日期时间选择对话框封装类 + * 继承自AlertDialog,核心职责: + * 1. 集成自定义DateTimePicker控件,提供统一的日期时间选择UI; + * 2. 管理选中的日期时间(Calendar对象),处理时间变化的实时更新; + * 3. 适配系统24小时制设置,动态更新对话框标题(格式化显示选中的日期时间); + * 4. 提供选择完成的回调接口,返回最终选中的时间戳; + * 典型使用场景:便签添加/编辑页面的“设置提醒时间”功能。 + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + // 选中的日期时间对象:存储用户选择的年/月/日/时/分,秒固定为0 private Calendar mDate = Calendar.getInstance(); + // 是否使用24小时制:适配系统设置,影响时间显示格式 private boolean mIs24HourView; + // 日期时间选择完成的回调监听器:外部实现,接收最终选中的时间戳 private OnDateTimeSetListener mOnDateTimeSetListener; + // 核心日期时间选择控件:承载年/月/日/时/分的选择UI private DateTimePicker mDateTimePicker; + /** + * 日期时间选择完成的回调接口 + * 由外部(如NoteEditActivity)实现,接收对话框返回的选中时间戳 + */ public interface OnDateTimeSetListener { + /** + * 选择完成回调方法 + * @param dialog 当前的日期时间选择对话框(可用于关闭/操作对话框) + * @param date 选中的日期时间戳(毫秒级,秒已置为0) + */ void OnDateTimeSet(AlertDialog dialog, long date); } + /** + * 构造方法:初始化日期时间选择对话框 + * @param context 应用上下文:用于创建对话框、加载资源、适配系统设置 + * @param date 初始时间戳:对话框打开时默认选中的时间(毫秒级) + */ public DateTimePickerDialog(Context context, long date) { super(context); + // 1. 创建自定义日期时间选择控件,设置为对话框的核心视图 mDateTimePicker = new DateTimePicker(context); setView(mDateTimePicker); + + // 2. 绑定DateTimePicker的时间变化监听:实时更新选中的Calendar对象 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { + // 更新Calendar对象的年/月/日/时/分 mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); mDate.set(Calendar.MINUTE, minute); + // 实时更新对话框标题(格式化显示当前选中的时间) updateTitle(mDate.getTimeInMillis()); } }); + + // 3. 初始化选中的日期时间:设置初始时间戳,秒固定为0(仅保留到分钟级) mDate.setTimeInMillis(date); mDate.set(Calendar.SECOND, 0); + // 将初始时间设置到DateTimePicker控件 mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); - setButton(context.getString(R.string.datetime_dialog_ok), this); - setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + + // 4. 设置对话框按钮:确认(触发回调)、取消(无操作) + setButton(context.getString(R.string.datetime_dialog_ok), this); // 确认按钮绑定当前类的onClick + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮无操作 + + // 5. 适配系统24小时制设置:获取系统是否启用24小时制,设置到对话框 set24HourView(DateFormat.is24HourFormat(this.getContext())); + + // 6. 初始化对话框标题:格式化显示初始时间 updateTitle(mDate.getTimeInMillis()); } + /** + * 设置是否使用24小时制显示时间 + * @param is24HourView true=24小时制,false=12小时制(上午/下午) + */ public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; } + /** + * 设置日期时间选择完成的回调监听器 + * @param callBack 外部实现的OnDateTimeSetListener(如NoteEditActivity) + */ public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; } + /** + * 私有方法:更新对话框标题,格式化显示选中的日期时间 + * @param date 要显示的时间戳(毫秒级) + */ private void updateTitle(long date) { + // 定义日期时间格式化标记:显示年、日期、时间 int flag = - DateUtils.FORMAT_SHOW_YEAR | - DateUtils.FORMAT_SHOW_DATE | - DateUtils.FORMAT_SHOW_TIME; + DateUtils.FORMAT_SHOW_YEAR | // 显示年份 + DateUtils.FORMAT_SHOW_DATE | // 显示日期(月/日) + DateUtils.FORMAT_SHOW_TIME; // 显示时间(时/分) + // 适配24小时制:覆盖标记(FORMAT_24HOUR无论true/false,仅控制显示格式) flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + // 格式化时间戳为文本,设置为对话框标题 setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } + /** + * 对话框确认按钮点击事件处理 + * 触发日期时间选择完成的回调,返回最终选中的时间戳 + * @param arg0 对话框实例(DialogInterface) + * @param arg1 按钮索引(确认按钮为DialogInterface.BUTTON_POSITIVE) + */ public void onClick(DialogInterface arg0, int arg1) { + // 有回调监听器时,触发回调并传递选中的时间戳 if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } diff --git a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..9316cf4 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java @@ -14,48 +14,95 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,封装下拉菜单的核心逻辑,简化PopupMenu的使用 package net.micode.notes.ui; +// 导入安卓上下文类:提供应用运行环境(创建PopupMenu、加载资源) import android.content.Context; +// 导入安卓菜单相关类:管理下拉菜单的选项和菜单项 import android.view.Menu; import android.view.MenuItem; +// 导入安卓视图相关类:处理按钮点击事件 import android.view.View; import android.view.View.OnClickListener; +// 导入安卓按钮控件类:作为下拉菜单的触发按钮 import android.widget.Button; +// 导入安卓PopupMenu类:核心的下拉菜单控件 import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; +// 导入小米便签资源类:引用下拉按钮的背景资源 import net.micode.notes.R; +/** + * 下拉菜单封装类 + * 核心职责: + * 1. 封装安卓PopupMenu和触发按钮(Button),简化下拉菜单的创建与使用; + * 2. 统一设置下拉按钮的样式(背景图标); + * 3. 封装菜单加载、点击事件绑定、菜单项查找、按钮文本设置等核心逻辑; + * 典型使用场景:便签应用中排序、筛选、操作选项等下拉菜单场景(如列表页的更多操作)。 + */ public class DropdownMenu { + // 下拉菜单的触发按钮:点击该按钮显示下拉菜单,统一设置背景样式 private Button mButton; + // 核心下拉菜单控件:承载菜单选项,控制菜单的显示/隐藏 private PopupMenu mPopupMenu; + // PopupMenu对应的Menu对象:用于查找菜单项、管理菜单内容 private Menu mMenu; + /** + * 构造方法:初始化下拉菜单(绑定触发按钮+加载菜单布局) + * @param context 应用上下文:用于创建PopupMenu、加载菜单资源 + * @param button 触发下拉菜单的按钮控件:统一设置背景为下拉图标 + * @param menuId 菜单布局资源ID:如R.menu.xxx,定义下拉菜单的选项列表 + */ public DropdownMenu(Context context, Button button, int menuId) { + // 保存触发按钮引用 mButton = button; + // 设置按钮背景为下拉图标(统一样式) mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建PopupMenu:关联上下文和触发按钮(菜单显示在按钮下方) mPopupMenu = new PopupMenu(context, mButton); + // 获取PopupMenu对应的Menu对象,用于后续菜单管理 mMenu = mPopupMenu.getMenu(); + // 加载菜单布局资源到Menu中(初始化下拉菜单的选项) mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 绑定按钮点击事件:点击按钮显示下拉菜单 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { - mPopupMenu.show(); + mPopupMenu.show(); // 显示下拉菜单 } }); } + /** + * 设置下拉菜单项的点击监听器 + * 监听器由外部实现,处理不同菜单项的点击逻辑(如排序、删除、移动等) + * @param listener 菜单项点击监听器(OnMenuItemClickListener) + */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + // 仅当PopupMenu初始化完成时,绑定监听器 if (mPopupMenu != null) { mPopupMenu.setOnMenuItemClickListener(listener); } } + /** + * 根据菜单项ID查找对应的MenuItem + * 用于外部动态修改菜单项状态(如隐藏/禁用/设置图标) + * @param id 菜单项的资源ID(如R.id.menu_sort_by_time) + * @return MenuItem:找到的菜单项(未找到返回null) + */ public MenuItem findItem(int id) { return mMenu.findItem(id); } + /** + * 设置下拉触发按钮的显示文本 + * 用于动态更新按钮文本(如“排序方式”“当前文件夹”等) + * @param title 按钮要显示的文本内容 + */ public void setTitle(CharSequence title) { mButton.setText(title); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..332c975 100644 --- a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java @@ -14,67 +14,141 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,作为文件夹列表(如“移动便签”弹窗)的核心适配器 package net.micode.notes.ui; +// 导入安卓上下文类:提供应用运行环境(创建列表项、加载资源) import android.content.Context; +// 导入安卓数据库游标类:存储文件夹数据库查询结果 import android.database.Cursor; +// 导入安卓视图相关类:创建/绑定列表项视图 import android.view.View; import android.view.ViewGroup; +// 导入安卓Cursor适配器类:适配Cursor数据的列表适配器基类 import android.widget.CursorAdapter; +// 导入安卓线性布局类:作为文件夹列表项的根布局 import android.widget.LinearLayout; +// 导入安卓文本视图类:展示文件夹名称 import android.widget.TextView; +// 导入小米便签资源类:引用布局、字符串资源(根文件夹显示文本) import net.micode.notes.R; +// 导入便签数据常量类:定义文件夹ID(根文件夹)、字段等常量 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; - +/** + * 文件夹列表核心适配器类 + * 继承自安卓CursorAdapter,核心职责: + * 1. 定义文件夹数据库查询的投影字段(仅ID和名称),减少查询冗余; + * 2. 将文件夹Cursor数据绑定到自定义列表项(FolderListItem); + * 3. 特殊适配根文件夹:将根文件夹的名称替换为“移动到上级文件夹”(而非原始名称); + * 4. 提供工具方法,根据列表位置获取文件夹名称(适配根文件夹规则)。 + * 典型使用场景:便签“移动到文件夹”弹窗的文件夹列表展示。 + */ public class FoldersListAdapter extends CursorAdapter { + /** + * 文件夹数据库查询投影数组:仅包含核心字段,减少IO开销 + * 字段说明: + * - NoteColumns.ID:文件夹唯一ID(根文件夹为Notes.ID_ROOT_FOLDER) + * - NoteColumns.SNIPPET:文件夹名称(根文件夹的SNIPPET无业务意义,需特殊替换) + */ public static final String [] PROJECTION = { - NoteColumns.ID, - NoteColumns.SNIPPET + NoteColumns.ID, // 0: 文件夹唯一ID + NoteColumns.SNIPPET // 1: 文件夹名称 }; - public static final int ID_COLUMN = 0; - public static final int NAME_COLUMN = 1; + // 投影数组对应的列索引常量:简化Cursor取值,避免硬编码索引 + public static final int ID_COLUMN = 0; // 文件夹ID列索引 + public static final int NAME_COLUMN = 1; // 文件夹名称列索引 + /** + * 构造方法:初始化文件夹列表适配器 + * @param context 应用上下文,传递给父类(用于创建列表项、加载资源) + * @param c 包含文件夹数据的Cursor(已查询完成,包含ID和名称字段) + */ public FoldersListAdapter(Context context, Cursor c) { super(context, c); // TODO Auto-generated constructor stub } + /** + * 重写创建新视图方法:创建文件夹列表项视图 + * @param context 应用上下文 + * @param cursor 当前位置的Cursor(未使用,仅遵循父类接口) + * @param parent 列表项的父容器(ListView) + * @return View:新创建的FolderListItem自定义列表项 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { + // 创建自定义文件夹列表项视图(FolderListItem) return new FolderListItem(context); } + /** + * 重写绑定视图方法:将文件夹Cursor数据绑定到列表项UI + * 核心逻辑: + * - 根文件夹:名称替换为“移动到上级文件夹”; + * - 普通文件夹:使用Cursor中的原始名称; + * - 将处理后的名称绑定到列表项的文本控件。 + * @param view 要绑定的列表项视图(FolderListItem) + * @param context 应用上下文(加载根文件夹的显示文本) + * @param cursor 当前位置的Cursor,包含文件夹ID和名称 + */ @Override public void bindView(View view, Context context, Cursor cursor) { + // 仅处理FolderListItem类型的视图(防止类型错误) if (view instanceof FolderListItem) { + // 处理文件夹名称:根文件夹替换为“移动到上级文件夹”,普通文件夹用原始名称 String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + // 调用列表项的bind方法,设置文件夹名称 ((FolderListItem) view).bind(folderName); } } + /** + * 工具方法:根据列表位置获取文件夹名称(适配根文件夹规则) + * 用于外部(如弹窗逻辑)快速获取指定位置的文件夹名称,无需手动解析Cursor + * @param context 应用上下文(加载根文件夹的显示文本) + * @param position 列表项位置 + * @return String:处理后的文件夹名称(根文件夹为“移动到上级文件夹”) + */ public String getFolderName(Context context, int position) { + // 获取指定位置的Cursor Cursor cursor = (Cursor) getItem(position); + // 同bindView逻辑:根文件夹替换名称,普通文件夹用原始名称 return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); } + /** + * 内部类:文件夹列表项自定义布局 + * 继承自LinearLayout,核心职责:初始化列表项布局和文本控件,提供名称绑定方法。 + */ private class FolderListItem extends LinearLayout { + // 文件夹名称文本控件:展示处理后的文件夹名称 private TextView mName; + /** + * 构造方法:初始化列表项布局和控件 + * @param context 应用上下文,用于加载布局和查找控件 + */ public FolderListItem(Context context) { super(context); + // 加载文件夹列表项布局(res/layout/folder_list_item.xml)到当前LinearLayout inflate(context, R.layout.folder_list_item, this); + // 初始化文件夹名称文本控件 mName = (TextView) findViewById(R.id.tv_folder_name); } + /** + * 绑定方法:设置文件夹名称到文本控件 + * @param name 处理后的文件夹名称(根文件夹为“移动到上级文件夹”) + */ public void bind(String name) { mName.setText(name); } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..5235dc5 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java @@ -14,96 +14,172 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,自定义EditText适配便签编辑的特殊交互逻辑 package net.micode.notes.ui; +// 导入安卓上下文类:提供应用运行环境 import android.content.Context; +// 导入安卓图形矩形类:用于焦点变化时的区域计算 import android.graphics.Rect; +// 导入安卓文本布局类:处理文本的行/列定位(触摸光标定位) import android.text.Layout; +// 导入安卓文本选择类:控制文本光标位置 import android.text.Selection; +// 导入安卓富文本类:处理包含URLSpan的富文本 import android.text.Spanned; +// 导入安卓文本工具类:判空等文本操作 import android.text.TextUtils; +// 导入安卓URLSpan类:处理文本中的链接(电话/网页/邮箱) import android.text.style.URLSpan; +// 导入安卓属性集类:自定义View构造参数 import android.util.AttributeSet; +// 导入安卓日志类:输出编辑框相关日志 import android.util.Log; +// 导入安卓上下文菜单类:自定义链接的上下文菜单 import android.view.ContextMenu; +// 导入安卓按键事件类:处理回车/删除等按键交互 import android.view.KeyEvent; +// 导入安卓菜单项类:构建上下文菜单的选项 import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; +// 导入安卓触摸事件类:处理触摸定位光标 import android.view.MotionEvent; +// 导入安卓编辑框类:自定义EditText的父类 import android.widget.EditText; +// 导入小米便签资源类:引用字符串资源(链接菜单文本) import net.micode.notes.R; +// 导入集合类:存储scheme与菜单文本的映射关系 import java.util.HashMap; import java.util.Map; +/** + * 便签编辑页自定义EditText类 + * 继承自安卓EditText,核心扩展职责: + * 1. 适配便签编辑的按键交互: + * - 删除键:光标在起始位置且非首个编辑框时,回调删除当前编辑框; + * - 回车键:分割文本并回调新增编辑框; + * 2. 自定义触摸事件:精准定位触摸位置的光标,优化富文本编辑体验; + * 3. 处理富文本链接(电话/网页/邮箱):长按链接弹出自定义上下文菜单,支持跳转; + * 4. 焦点变化回调:编辑框失焦且为空时,回调隐藏相关选项; + * 5. 管理编辑框索引:支持多编辑框的增删交互。 + */ public class NoteEditText extends EditText { + // 日志标签:用于编辑框相关日志输出 private static final String TAG = "NoteEditText"; + // 当前编辑框在多编辑框列表中的索引(用于增删回调) private int mIndex; + // 删除键按下前的光标起始位置(用于判断是否触发编辑框删除) private int mSelectionStartBeforeDelete; - private static final String SCHEME_TEL = "tel:" ; - private static final String SCHEME_HTTP = "http:" ; - private static final String SCHEME_EMAIL = "mailto:" ; + // 链接Scheme常量:匹配文本中的不同类型链接 + private static final String SCHEME_TEL = "tel:" ; // 电话链接前缀 + private static final String SCHEME_HTTP = "http:" ; // 网页链接前缀 + private static final String SCHEME_EMAIL = "mailto:" ;// 邮箱链接前缀 + /** + * Scheme与上下文菜单文本资源的映射表 + * 用于根据链接类型(电话/网页/邮箱)显示对应的菜单文本 + */ private static final Map sSchemaActionResMap = new HashMap(); static { - sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); - sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); - sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + // 初始化映射关系:Scheme -> 菜单文本资源ID + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 → “拨打电话” + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 → “打开网页” + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);// 邮箱链接 → “发送邮件” } /** - * Call by the {@link NoteEditActivity} to delete or add edit text + * 编辑框状态变化回调接口 + * 由NoteEditActivity实现,处理编辑框的增删、文本变化交互 */ public interface OnTextViewChangeListener { /** - * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens - * and the text is null + * 删除当前编辑框的回调 + * 触发条件:按下删除键 + 光标在起始位置 + 非首个编辑框 + * @param index 当前编辑框的索引 + * @param text 当前编辑框的文本内容 */ void onEditTextDelete(int index, String text); /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen + * 新增编辑框的回调 + * 触发条件:按下回车键 + * @param index 新增编辑框的目标索引(当前索引+1) + * @param text 分割后的文本(回车后的内容) */ void onEditTextEnter(int index, String text); /** - * Hide or show item option when text change + * 文本变化/焦点变化的回调 + * 用于控制编辑框相关选项的显示/隐藏 + * @param index 当前编辑框的索引 + * @param hasText 是否有文本(true=显示选项,false=隐藏选项) */ void onTextChange(int index, boolean hasText); } + // 编辑框状态变化监听器(由Activity实现) private OnTextViewChangeListener mOnTextViewChangeListener; + /** + * 构造方法:初始化单个编辑框(无属性集) + * @param context 应用上下文 + */ public NoteEditText(Context context) { super(context, null); + // 默认索引为0(首个编辑框) mIndex = 0; } + /** + * 设置当前编辑框的索引 + * @param index 多编辑框列表中的索引 + */ public void setIndex(int index) { mIndex = index; } + /** + * 设置编辑框状态变化监听器 + * @param listener 实现OnTextViewChangeListener的监听器(通常为NoteEditActivity) + */ public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } + /** + * 构造方法:带属性集的初始化(布局文件引用时调用) + * @param context 应用上下文 + * @param attrs 布局属性集 + */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } + /** + * 构造方法:带属性集和默认样式的初始化 + * @param context 应用上下文 + * @param attrs 布局属性集 + * @param defStyle 默认样式 + */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub } + /** + * 重写触摸事件:精准定位触摸位置的光标 + * 核心逻辑:将触摸坐标转换为文本的行/列偏移,设置光标位置 + * @param event 触摸事件 + * @return boolean:是否消费触摸事件(此处返回父类处理结果) + */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - + // 1. 计算触摸的相对坐标(扣除内边距,加上滚动偏移) int x = (int) event.getX(); int y = (int) event.getY(); x -= getTotalPaddingLeft(); @@ -111,86 +187,136 @@ public class NoteEditText extends EditText { x += getScrollX(); y += getScrollY(); + // 2. 根据坐标获取文本的行和偏移量 Layout layout = getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); + int line = layout.getLineForVertical(y); // 触摸位置的文本行 + int off = layout.getOffsetForHorizontal(line, x); // 该行的水平偏移量 + + // 3. 设置光标到触摸位置 Selection.setSelection(getText(), off); break; } + // 交给父类处理剩余触摸逻辑(如滑动、长按) return super.onTouchEvent(event); } + /** + * 重写按键按下事件:预处理回车/删除键 + * @param keyCode 按键码(KEYCODE_ENTER/KEYCODE_DEL等) + * @param event 按键事件 + * @return boolean:是否消费按键事件 + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: + // 有监听器时返回false,交给onKeyUp处理(分割文本+新增编辑框) if (mOnTextViewChangeListener != null) { return false; } break; case KeyEvent.KEYCODE_DEL: + // 记录删除键按下前的光标位置,用于onKeyUp判断是否触发删除编辑框 mSelectionStartBeforeDelete = getSelectionStart(); break; default: break; } + // 交给父类处理其他按键逻辑 return super.onKeyDown(keyCode, event); } + /** + * 重写按键抬起事件:处理回车/删除键的核心交互逻辑 + * @param keyCode 按键码 + * @param event 按键事件 + * @return boolean:是否消费按键事件 + */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_DEL: + // 有监听器时处理删除逻辑 if (mOnTextViewChangeListener != null) { + // 触发条件:光标在起始位置 + 非首个编辑框(索引≠0) if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + // 回调删除当前编辑框 mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); - return true; + return true; // 消费事件,避免父类处理 } } else { + // 无监听器时输出日志提示 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; case KeyEvent.KEYCODE_ENTER: + // 有监听器时处理回车逻辑 if (mOnTextViewChangeListener != null) { + // 获取光标起始位置 int selectionStart = getSelectionStart(); + // 分割文本:光标后的内容作为新增编辑框的文本 String text = getText().subSequence(selectionStart, length()).toString(); + // 保留光标前的内容在当前编辑框 setText(getText().subSequence(0, selectionStart)); + // 回调新增编辑框(索引为当前+1) mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { + // 无监听器时输出日志提示 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; default: break; } + // 交给父类处理其他按键逻辑 return super.onKeyUp(keyCode, event); } + /** + * 重写焦点变化事件:回调文本状态(控制选项显示/隐藏) + * @param focused 是否获取焦点 + * @param direction 焦点变化方向 + * @param previouslyFocusedRect 之前的焦点区域 + */ @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener != null) { + // 失焦且文本为空:回调隐藏选项;否则回调显示选项 if (!focused && TextUtils.isEmpty(getText())) { mOnTextViewChangeListener.onTextChange(mIndex, false); } else { mOnTextViewChangeListener.onTextChange(mIndex, true); } } + // 交给父类处理焦点变化 super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * 重写上下文菜单创建方法:自定义链接的上下文菜单 + * 核心逻辑:识别选中区域的URLSpan,根据Scheme显示对应的菜单选项,点击触发链接跳转 + * @param menu 上下文菜单对象 + */ @Override protected void onCreateContextMenu(ContextMenu menu) { + // 仅处理富文本(Spanned)类型的文本 if (getText() instanceof Spanned) { + // 获取光标选中的起始/结束位置 int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); + // 修正选中范围(确保min≤max) int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); + // 获取选中区域内的所有URLSpan(链接) final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + // 仅处理单个链接的情况(避免多链接冲突) if (urls.length == 1) { + // 默认菜单文本资源ID(未知链接) int defaultResId = 0; + // 匹配链接的Scheme,获取对应的菜单文本 for(String schema: sSchemaActionResMap.keySet()) { if(urls[0].getURL().indexOf(schema) >= 0) { defaultResId = sSchemaActionResMap.get(schema); @@ -198,20 +324,23 @@ public class NoteEditText extends EditText { } } + // 未匹配到已知Scheme:使用“其他链接”文本 if (defaultResId == 0) { defaultResId = R.string.note_link_other; } + // 添加菜单选项并设置点击事件 menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // goto a new intent + // 触发URLSpan的点击事件(跳转链接:拨打电话/打开网页/发送邮件) urls[0].onClick(NoteEditText.this); return true; } }); } } + // 交给父类创建默认上下文菜单 super.onCreateContextMenu(menu); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java index 0f5a878..82cc2fe 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java @@ -14,211 +14,355 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,作为便签列表项的核心数据模型,解析并封装Cursor中的便签数据 package net.micode.notes.ui; +// 导入安卓上下文类:用于获取ContentResolver、查询联系人信息 import android.content.Context; +// 导入安卓数据库游标类:存储便签数据库查询结果,作为数据解析源 import android.database.Cursor; +// 导入安卓文本工具类:处理字符串判空、替换等操作 import android.text.TextUtils; +// 导入联系人数据类:根据手机号查询联系人名称 import net.micode.notes.data.Contact; +// 导入便签数据常量类:定义便签类型、特殊文件夹ID(通话记录文件夹)等常量 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; +// 导入数据工具类:获取通话记录手机号、格式化数据等 import net.micode.notes.tool.DataUtils; - +/** + * 便签列表项核心数据模型类 + * 核心职责: + * 1. 定义便签数据库查询的投影字段(PROJECTION),指定需要解析的核心字段; + * 2. 从Cursor中解析便签/文件夹的所有核心数据(ID、类型、背景色、时间、摘要等); + * 3. 适配通话记录项:解析手机号并查询对应的联系人名称; + * 4. 判断当前项在列表中的位置状态(首/尾/唯一项、文件夹后的便签项类型); + * 5. 提供数据访问方法(getter),封装数据逻辑(如是否有提醒、是否为通话记录)。 + */ public class NoteItemData { + /** + * 数据库查询投影数组:指定需要解析的便签核心字段,减少查询冗余 + * 覆盖便签/文件夹的基础属性、时间属性、关联属性(小部件、文件夹)、扩展属性(附件、提醒) + */ static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE, - NoteColumns.BG_COLOR_ID, - NoteColumns.CREATED_DATE, - NoteColumns.HAS_ATTACHMENT, - NoteColumns.MODIFIED_DATE, - NoteColumns.NOTES_COUNT, - NoteColumns.PARENT_ID, - NoteColumns.SNIPPET, - NoteColumns.TYPE, - NoteColumns.WIDGET_ID, - NoteColumns.WIDGET_TYPE, + NoteColumns.ID, // 0: 便签/文件夹唯一ID + NoteColumns.ALERTED_DATE, // 1: 提醒时间戳(0表示无提醒) + NoteColumns.BG_COLOR_ID, // 2: 背景色ID + NoteColumns.CREATED_DATE, // 3: 创建时间戳 + NoteColumns.HAS_ATTACHMENT, // 4: 是否有附件(0=无,1=有) + NoteColumns.MODIFIED_DATE, // 5: 最后修改时间戳 + NoteColumns.NOTES_COUNT, // 6: 文件夹内便签数量(仅文件夹有效) + NoteColumns.PARENT_ID, // 7: 父文件夹ID(根文件夹为Notes.ID_ROOT_FOLDER) + NoteColumns.SNIPPET, // 8: 便签摘要/文件夹名称 + NoteColumns.TYPE, // 9: 类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) + NoteColumns.WIDGET_ID, // 10: 关联的小部件ID(无效为AppWidgetManager.INVALID_APPWIDGET_ID) + NoteColumns.WIDGET_TYPE, // 11: 关联的小部件类型(2x/4x,对应Notes.TYPE_WIDGET_2X/4X) }; - private static final int ID_COLUMN = 0; - private static final int ALERTED_DATE_COLUMN = 1; - private static final int BG_COLOR_ID_COLUMN = 2; - private static final int CREATED_DATE_COLUMN = 3; - private static final int HAS_ATTACHMENT_COLUMN = 4; - private static final int MODIFIED_DATE_COLUMN = 5; - private static final int NOTES_COUNT_COLUMN = 6; - private static final int PARENT_ID_COLUMN = 7; - private static final int SNIPPET_COLUMN = 8; - private static final int TYPE_COLUMN = 9; - private static final int WIDGET_ID_COLUMN = 10; - private static final int WIDGET_TYPE_COLUMN = 11; - - private long mId; - private long mAlertDate; - private int mBgColorId; - private long mCreatedDate; - private boolean mHasAttachment; - private long mModifiedDate; - private int mNotesCount; - private long mParentId; - private String mSnippet; - private int mType; - private int mWidgetId; - private int mWidgetType; - private String mName; - private String mPhoneNumber; - - private boolean mIsLastItem; - private boolean mIsFirstItem; - private boolean mIsOnlyOneItem; - private boolean mIsOneNoteFollowingFolder; - private boolean mIsMultiNotesFollowingFolder; - + // 投影数组对应的列索引常量:简化Cursor取值,避免硬编码索引 + private static final int ID_COLUMN = 0; // 便签/文件夹ID列索引 + private static final int ALERTED_DATE_COLUMN = 1; // 提醒时间列索引 + private static final int BG_COLOR_ID_COLUMN = 2; // 背景色ID列索引 + private static final int CREATED_DATE_COLUMN = 3; // 创建时间列索引 + private static final int HAS_ATTACHMENT_COLUMN = 4; // 是否有附件列索引 + private static final int MODIFIED_DATE_COLUMN = 5; // 最后修改时间列索引 + private static final int NOTES_COUNT_COLUMN = 6; // 文件夹内便签数量列索引 + private static final int PARENT_ID_COLUMN = 7; // 父文件夹ID列索引 + private static final int SNIPPET_COLUMN = 8; // 摘要/文件夹名称列索引 + private static final int TYPE_COLUMN = 9; // 类型列索引 + private static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引 + private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引 + + // 核心数据字段:与投影数组一一对应 + private long mId; // 便签/文件夹唯一ID + private long mAlertDate; // 提醒时间戳(>0表示有提醒) + private int mBgColorId; // 背景色ID(用于列表项背景渲染) + private long mCreatedDate; // 创建时间戳 + private boolean mHasAttachment; // 是否有附件(图片/音频等) + private long mModifiedDate; // 最后修改时间戳(用于列表项时间展示) + private int mNotesCount; // 文件夹内便签数量(仅TYPE_FOLDER有效) + private long mParentId; // 父文件夹ID(通话记录项为Notes.ID_CALL_RECORD_FOLDER) + private String mSnippet; // 便签摘要/文件夹名称(清理了勾选标记后的纯文本) + private int mType; // 类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) + private int mWidgetId; // 关联的小部件ID(无效为INVALID_APPWIDGET_ID) + private int mWidgetType; // 关联的小部件类型(2x/4x) + private String mName; // 通话记录项的联系人名称(无则显示手机号) + private String mPhoneNumber; // 通话记录项的手机号(仅父ID为通话记录文件夹有效) + + // 列表位置状态字段:用于适配列表项背景渲染 + private boolean mIsLastItem; // 是否为列表最后一项 + private boolean mIsFirstItem; // 是否为列表第一项 + private boolean mIsOnlyOneItem; // 是否为列表唯一一项 + private boolean mIsOneNoteFollowingFolder; // 是否为文件夹后的唯一便签项 + private boolean mIsMultiNotesFollowingFolder;// 是否为文件夹后的多个便签项的第一项 + + /** + * 构造方法:从Cursor解析便签/文件夹数据,初始化所有字段 + * @param context 应用上下文:用于查询ContentResolver、获取联系人信息 + * @param cursor 包含便签数据的Cursor(已移动到目标位置) + */ public NoteItemData(Context context, Cursor cursor) { + // 1. 解析Cursor核心字段:与投影数组索引一一对应 mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + // 转换int为boolean:0=无附件,>0=有附件 mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + // 清理摘要中的勾选标记(TAG_CHECKED/TAG_UNCHECKED),仅保留纯文本 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); mType = cursor.getInt(TYPE_COLUMN); mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + // 2. 初始化通话记录相关字段:仅父ID为通话记录文件夹时解析 mPhoneNumber = ""; if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + // 根据便签ID查询对应的通话记录手机号 mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + // 有手机号时,查询对应的联系人名称 if (!TextUtils.isEmpty(mPhoneNumber)) { mName = Contact.getContact(context, mPhoneNumber); + // 无联系人名称时,使用手机号作为名称 if (mName == null) { mName = mPhoneNumber; } } } + // 3. 兜底:联系人名称为空时设为空字符串,避免空指针 if (mName == null) { mName = ""; } + + // 4. 判断当前项在列表中的位置状态(用于背景渲染) checkPostion(cursor); } + /** + * 私有方法:判断当前项在Cursor中的位置状态,初始化位置相关字段 + * 核心逻辑: + * 1. 判断是否为首/尾/唯一项; + * 2. 判断普通便签是否为文件夹后的第一项(单个/多个)。 + * @param cursor 包含便签数据的Cursor(已移动到目标位置) + */ private void checkPostion(Cursor cursor) { - mIsLastItem = cursor.isLast() ? true : false; - mIsFirstItem = cursor.isFirst() ? true : false; - mIsOnlyOneItem = (cursor.getCount() == 1); + // 初始化基础位置状态 + mIsLastItem = cursor.isLast() ? true : false; // 是否为最后一项 + mIsFirstItem = cursor.isFirst() ? true : false; // 是否为第一项 + mIsOnlyOneItem = (cursor.getCount() == 1); // 是否为唯一一项 + // 初始化文件夹后便签项状态为false mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; + // 仅处理普通便签且非第一项的情况:判断是否为文件夹后的便签项 if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + // 记录当前位置 int position = cursor.getPosition(); + // 移动Cursor到上一项,判断上一项是否为文件夹/系统项 if (cursor.moveToPrevious()) { if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 上一项是文件夹/系统项:判断当前项后是否还有更多项 if (cursor.getCount() > (position + 1)) { + // 有更多项:标记为“文件夹后的多个便签项的第一项” mIsMultiNotesFollowingFolder = true; } else { + // 无更多项:标记为“文件夹后的唯一便签项” mIsOneNoteFollowingFolder = true; } } + // 移动Cursor回原位置,避免影响后续操作 if (!cursor.moveToNext()) { + // 移动失败时抛出异常,防止Cursor位置错乱 throw new IllegalStateException("cursor move to previous but can't move back"); } } } } + /** + * 判断是否为文件夹后的唯一便签项 + * @return boolean:true=是,false=否 + */ public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } + /** + * 判断是否为文件夹后的多个便签项的第一项 + * @return boolean:true=是,false=否 + */ public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } + /** + * 判断是否为列表最后一项 + * @return boolean:true=是,false=否 + */ public boolean isLast() { return mIsLastItem; } + /** + * 获取通话记录项的联系人名称(无则返回手机号) + * @return String:联系人名称/手机号 + */ public String getCallName() { return mName; } + /** + * 判断是否为列表第一项 + * @return boolean:true=是,false=否 + */ public boolean isFirst() { return mIsFirstItem; } + /** + * 判断是否为列表唯一一项 + * @return boolean:true=是,false=否 + */ public boolean isSingle() { return mIsOnlyOneItem; } + /** + * 获取便签/文件夹唯一ID + * @return long:ID值 + */ public long getId() { return mId; } + /** + * 获取提醒时间戳 + * @return long:提醒时间戳(>0表示有提醒) + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取创建时间戳 + * @return long:创建时间戳 + */ public long getCreatedDate() { return mCreatedDate; } + /** + * 判断是否有附件 + * @return boolean:true=有,false=无 + */ public boolean hasAttachment() { return mHasAttachment; } + /** + * 获取最后修改时间戳 + * @return long:最后修改时间戳 + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景色ID + * @return int:背景色ID(用于列表项背景渲染) + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取父文件夹ID + * @return long:父文件夹ID(通话记录项为Notes.ID_CALL_RECORD_FOLDER) + */ public long getParentId() { return mParentId; } + /** + * 获取文件夹内便签数量(仅TYPE_FOLDER有效) + * @return int:便签数量 + */ public int getNotesCount() { return mNotesCount; } + /** + * 兼容方法:获取父文件夹ID(与getParentId逻辑一致,适配外部调用) + * @return long:父文件夹ID + */ public long getFolderId () { return mParentId; } + /** + * 获取便签/文件夹类型 + * @return int:类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) + */ public int getType() { return mType; } + /** + * 获取关联的小部件类型 + * @return int:小部件类型(2x/4x,对应Notes.TYPE_WIDGET_2X/4X) + */ public int getWidgetType() { return mWidgetType; } + /** + * 获取关联的小部件ID + * @return int:小部件ID(无效为AppWidgetManager.INVALID_APPWIDGET_ID) + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取清理后的便签摘要/文件夹名称 + * @return String:纯文本摘要/名称 + */ public String getSnippet() { return mSnippet; } + /** + * 判断是否有提醒 + * @return boolean:true=有提醒(mAlertDate>0),false=无提醒 + */ public boolean hasAlert() { return (mAlertDate > 0); } + /** + * 判断是否为通话记录项 + * @return boolean:true=父ID为通话记录文件夹且有手机号,false=否 + */ public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + /** + * 静态工具方法:从Cursor中获取便签类型 + * 适配外部(如NotesListAdapter)快速获取类型,无需创建NoteItemData实例 + * @param cursor 包含便签数据的Cursor(已移动到目标位置) + * @return int:便签类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) + */ public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..2f4444a 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java @@ -14,112 +14,208 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,作为便签列表页的核心适配器,绑定Cursor数据到列表项 package net.micode.notes.ui; +// 导入安卓上下文类:提供应用运行环境 import android.content.Context; +// 导入安卓数据库游标类:存储便签数据库查询结果 import android.database.Cursor; +// 导入安卓日志类:输出适配器相关日志(异常/调试) import android.util.Log; +// 导入安卓视图相关类:创建/绑定列表项视图 import android.view.View; import android.view.ViewGroup; +// 导入安卓Cursor适配器类:适配Cursor数据的列表适配器基类 import android.widget.CursorAdapter; +// 导入便签数据常量类:定义便签类型、特殊ID(根文件夹)等常量 import net.micode.notes.data.Notes; +// 导入集合相关类:管理选中项的状态、统计选中数量 import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; - +/** + * 便签列表页核心适配器类 + * 继承自安卓CursorAdapter,核心职责: + * 1. 将便签数据库的Cursor数据绑定到自定义列表项(NotesListItem); + * 2. 管理列表的选择模式(批量操作):维护选中项状态、全选/取消全选、统计选中数量; + * 3. 提取选中项的核心信息(便签ID、关联的小部件属性); + * 4. 监听数据变化,实时更新普通便签的总数。 + */ public class NotesListAdapter extends CursorAdapter { + // 日志标签:用于适配器相关日志输出,便于问题定位 private static final String TAG = "NotesListAdapter"; + // 应用上下文:用于创建列表项、解析Cursor数据 private Context mContext; + // 选中项状态映射:Key=列表项位置,Value=是否选中(选择模式下) private HashMap mSelectedIndex; + // 普通便签总数:仅统计TYPE_NOTE类型的项,用于判断是否全选 private int mNotesCount; + // 选择模式标记:true=批量选择模式,false=普通浏览模式 private boolean mChoiceMode; + /** + * 内部静态类:存储便签关联的小部件属性 + * 用于批量操作时,提取选中便签绑定的小部件ID和类型 + */ public static class AppWidgetAttribute { - public int widgetId; - public int widgetType; + public int widgetId; // 小部件唯一标识ID + public int widgetType; // 小部件类型(2x/4x,对应Notes.TYPE_WIDGET_2X/4X) }; + /** + * 构造方法:初始化适配器核心参数 + * @param context 应用上下文,传递给父类并保存 + */ public NotesListAdapter(Context context) { + // 父类构造:上下文+初始Cursor(null,后续通过changeCursor设置) super(context, null); + // 初始化选中项状态映射(空HashMap) mSelectedIndex = new HashMap(); + // 保存上下文引用 mContext = context; + // 初始化普通便签总数为0 mNotesCount = 0; } + /** + * 重写创建新视图方法:创建便签列表项视图 + * @param context 应用上下文 + * @param cursor 当前位置的Cursor(未使用,仅遵循父类接口) + * @param parent 列表项的父容器(ListView) + * @return View:新创建的NotesListItem自定义列表项 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { + // 创建自定义列表项视图(NotesListItem) return new NotesListItem(context); } + /** + * 重写绑定视图方法:将Cursor数据绑定到列表项UI + * @param view 要绑定的列表项视图(NotesListItem) + * @param context 应用上下文 + * @param cursor 当前位置的Cursor,包含便签数据 + */ @Override public void bindView(View view, Context context, Cursor cursor) { + // 仅处理NotesListItem类型的视图(防止类型错误) if (view instanceof NotesListItem) { + // 根据Cursor创建便签数据模型(NoteItemData) NoteItemData itemData = new NoteItemData(context, cursor); + // 调用列表项的bind方法:绑定数据+适配选择模式+设置选中状态 ((NotesListItem) view).bind(context, itemData, mChoiceMode, isSelectedItem(cursor.getPosition())); } } + /** + * 设置指定位置列表项的选中状态 + * @param position 列表项位置 + * @param checked 是否选中 + */ public void setCheckedItem(final int position, final boolean checked) { + // 更新选中项状态映射 mSelectedIndex.put(position, checked); + // 通知ListView数据变化,刷新UI notifyDataSetChanged(); } + /** + * 判断当前是否为选择模式 + * @return boolean:true=选择模式,false=普通模式 + */ public boolean isInChoiceMode() { return mChoiceMode; } + /** + * 设置选择模式 + * @param mode true=开启选择模式,false=关闭选择模式 + */ public void setChoiceMode(boolean mode) { + // 清空原有选中项状态(切换模式时重置选择) mSelectedIndex.clear(); + // 更新选择模式标记 mChoiceMode = mode; } + /** + * 全选/取消全选操作 + * 仅对普通便签(TYPE_NOTE)生效,文件夹不参与选择 + * @param checked true=全选,false=取消全选 + */ public void selectAll(boolean checked) { + // 获取当前绑定的Cursor Cursor cursor = getCursor(); + // 遍历所有列表项 for (int i = 0; i < getCount(); i++) { + // 移动Cursor到当前位置 if (cursor.moveToPosition(i)) { + // 仅处理普通便签类型的项 if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + // 设置当前位置的选中状态 setCheckedItem(i, checked); } } } } + /** + * 获取选中项的便签ID集合 + * @return HashSet:选中的便签ID集合(过滤根文件夹ID) + */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); + // 遍历所有选中的位置 for (Integer position : mSelectedIndex.keySet()) { + // 仅处理选中状态为true的项 if (mSelectedIndex.get(position) == true) { + // 获取当前位置的便签ID Long id = getItemId(position); + // 过滤根文件夹ID(无效项,日志提示) if (id == Notes.ID_ROOT_FOLDER) { Log.d(TAG, "Wrong item id, should not happen"); } else { + // 添加有效ID到集合 itemSet.add(id); } } } - return itemSet; } + /** + * 获取选中项关联的小部件属性集合 + * @return HashSet:选中项的小部件属性集合(无效时返回null) + */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); + // 遍历所有选中的位置 for (Integer position : mSelectedIndex.keySet()) { + // 仅处理选中状态为true的项 if (mSelectedIndex.get(position) == true) { + // 获取当前位置的Cursor Cursor c = (Cursor) getItem(position); if (c != null) { + // 创建小部件属性对象 AppWidgetAttribute widget = new AppWidgetAttribute(); + // 根据Cursor创建便签数据模型 NoteItemData item = new NoteItemData(mContext, c); + // 提取小部件ID和类型 widget.widgetId = item.getWidgetId(); widget.widgetType = item.getWidgetType(); + // 添加到集合 itemSet.add(widget); /** - * Don't close cursor here, only the adapter could close it + * 此处不关闭Cursor:Cursor由适配器统一管理,外部关闭会导致数据异常 */ } else { + // 无效Cursor,输出错误日志并返回null Log.e(TAG, "Invalid cursor"); return null; } @@ -128,11 +224,18 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 统计选中项的数量 + * @return int:选中的普通便签数量 + */ public int getSelectedCount() { + // 获取所有选中状态值 Collection values = mSelectedIndex.values(); + // 无状态值时返回0 if (null == values) { return 0; } + // 遍历状态值,统计选中(true)的数量 Iterator iter = values.iterator(); int count = 0; while (iter.hasNext()) { @@ -143,42 +246,73 @@ public class NotesListAdapter extends CursorAdapter { return count; } + /** + * 判断是否全选 + * @return boolean:true=全选(选中数>0且等于普通便签总数),false=未全选 + */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); + // 选中数非0且等于普通便签总数,判定为全选 return (checkedCount != 0 && checkedCount == mNotesCount); } + /** + * 判断指定位置的列表项是否选中 + * @param position 列表项位置 + * @return boolean:true=选中,false=未选中(包括状态为null的情况) + */ public boolean isSelectedItem(final int position) { + // 状态为null时视为未选中 if (null == mSelectedIndex.get(position)) { return false; } + // 返回当前位置的选中状态 return mSelectedIndex.get(position); } + /** + * 重写内容变化回调方法:数据变化时更新普通便签总数 + * 当Cursor数据(便签数据库)发生变化时触发 + */ @Override protected void onContentChanged() { super.onContentChanged(); + // 重新计算普通便签总数 calcNotesCount(); } + /** + * 重写更换Cursor方法:更换Cursor后更新普通便签总数 + * @param cursor 新的Cursor + */ @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); + // 重新计算普通便签总数 calcNotesCount(); } + /** + * 私有方法:计算列表中普通便签(TYPE_NOTE)的总数 + * 用于判断全选状态,仅统计有效Cursor的普通便签项 + */ private void calcNotesCount() { + // 重置总数为0 mNotesCount = 0; + // 遍历所有列表项 for (int i = 0; i < getCount(); i++) { + // 获取当前位置的Cursor Cursor c = (Cursor) getItem(i); if (c != null) { + // 仅统计普通便签类型的项 if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { mNotesCount++; } } else { + // 无效Cursor,输出错误日志并终止计算 Log.e(TAG, "Invalid cursor"); return; } } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java index 1221e80..f605539 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java @@ -14,33 +14,64 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,负责便签列表页的单个列表项渲染 package net.micode.notes.ui; +// 导入安卓上下文类:提供应用运行环境(资源访问、样式加载) import android.content.Context; +// 导入安卓日期工具类:格式化相对时间(如“1分钟前”“昨天”) import android.text.format.DateUtils; +// 导入安卓视图类:控制控件可见性、状态 import android.view.View; +// 导入安卓复选框类:选择模式下展示的勾选框 import android.widget.CheckBox; +// 导入安卓图片视图类:展示提醒图标、通话记录图标 import android.widget.ImageView; +// 导入安卓线性布局类:作为列表项的根布局 import android.widget.LinearLayout; +// 导入安卓文本视图类:展示标题、时间、通话名称等文本 import android.widget.TextView; +// 导入小米便签资源类:引用布局、字符串、样式、图片等资源 import net.micode.notes.R; +// 导入便签数据常量类:定义便签/文件夹类型、特殊ID(通话记录文件夹)等常量 import net.micode.notes.data.Notes; +// 导入数据工具类:格式化便签摘要文本 import net.micode.notes.tool.DataUtils; +// 导入资源解析工具类:获取便签/文件夹的背景资源ID import net.micode.notes.tool.ResourceParser.NoteItemBgResources; - +/** + * 便签列表项自定义布局类 + * 继承自LinearLayout,作为便签列表页(NotesListActivity)的单个列表项容器,核心职责: + * 1. 初始化列表项的UI控件(提醒图标、标题、时间、通话名称、勾选框); + * 2. 根据便签数据类型(普通便签/文件夹/通话记录文件夹/通话记录项)渲染差异化UI; + * 3. 适配选择模式(显示/隐藏勾选框); + * 4. 根据便签状态(是否有提醒、背景色、列表位置)设置背景和图标。 + */ public class NotesListItem extends LinearLayout { + // 提醒/类型图标:展示闹钟提醒、通话记录等图标 private ImageView mAlert; + // 标题文本:展示便签摘要、文件夹名称(含数量)、通话记录标题 private TextView mTitle; + // 时间文本:展示便签最后修改的相对时间(如“5分钟前”) private TextView mTime; + // 通话名称文本:仅通话记录项展示来电/去电联系人名称 private TextView mCallName; + // 列表项绑定的便签数据模型 private NoteItemData mItemData; + // 选择模式下的勾选框:批量操作时展示 private CheckBox mCheckBox; + /** + * 构造方法:初始化列表项布局和控件 + * @param context 应用上下文,用于加载布局和查找控件 + */ public NotesListItem(Context context) { super(context); + // 加载列表项布局(res/layout/note_item.xml)到当前LinearLayout inflate(context, R.layout.note_item, this); + // 初始化各UI控件 mAlert = (ImageView) findViewById(R.id.iv_alert_icon); mTitle = (TextView) findViewById(R.id.tv_title); mTime = (TextView) findViewById(R.id.tv_time); @@ -48,7 +79,15 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } + /** + * 核心绑定方法:将便签数据绑定到列表项UI,适配不同类型/状态的展示逻辑 + * @param context 应用上下文,用于资源/样式加载 + * @param data 列表项对应的便签数据模型(NoteItemData) + * @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); @@ -56,36 +95,50 @@ public class NotesListItem extends LinearLayout { mCheckBox.setVisibility(View.GONE); } + // 保存当前绑定的数据模型 mItemData = data; + + // 分支1:通话记录文件夹(特殊ID) if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.GONE); - mAlert.setVisibility(View.VISIBLE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mCallName.setVisibility(View.GONE); // 隐藏通话名称 + mAlert.setVisibility(View.VISIBLE); // 显示类型图标 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题主样式 + // 标题:通话记录文件夹名称 + 文件夹内文件数量(格式化) mTitle.setText(context.getString(R.string.call_record_folder_name) + 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) { - mCallName.setVisibility(View.VISIBLE); - mCallName.setText(data.getCallName()); - mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mAlert.setImageResource(R.drawable.call_record); // 设置通话记录图标 + } + // 分支2:通话记录项(父ID为通话记录文件夹) + else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.VISIBLE); // 显示通话名称 + mCallName.setText(data.getCallName()); // 设置通话联系人名称 + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); // 设置标题次要样式 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 格式化显示便签摘要 + // 有提醒:显示闹钟图标;无提醒:隐藏图标 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); } else { mAlert.setVisibility(View.GONE); } - } else { - mCallName.setVisibility(View.GONE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + } + // 分支3:普通文件夹/普通便签(非通话记录相关) + else { + mCallName.setVisibility(View.GONE); // 隐藏通话名称 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题主样式 + // 子分支3.1:普通文件夹 if (data.getType() == Notes.TYPE_FOLDER) { + // 标题:文件夹名称 + 文件夹内文件数量(格式化) mTitle.setText(data.getSnippet() + context.getString(R.string.format_folder_files_count, data.getNotesCount())); - mAlert.setVisibility(View.GONE); - } else { - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mAlert.setVisibility(View.GONE); // 隐藏图标 + } + // 子分支3.2:普通便签 + else { + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 格式化显示便签摘要 + // 有提醒:显示闹钟图标;无提醒:隐藏图标 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); @@ -94,29 +147,49 @@ public class NotesListItem extends LinearLayout { } } } + // 设置最后修改时间:格式化为相对时间(如“1小时前”) mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + // 根据数据模型设置列表项背景 setBackground(data); } + /** + * 私有方法:根据便签数据设置列表项背景 + * 核心逻辑:区分普通便签/文件夹,普通便签再根据列表位置(首/中/尾/单独项)适配不同背景 + * @param data 列表项对应的便签数据模型 + */ 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()) { + // 列表最后一项:使用“最后项”背景 setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // 列表第一项/文件夹下多项的第一项:使用“第一项”背景 setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); } else { + // 列表中间项:使用“普通项”背景 setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } - } else { + } + // 文件夹(含通话记录文件夹):使用文件夹统一背景 + else { setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } + /** + * 获取当前列表项绑定的便签数据模型 + * @return NoteItemData:当前项的数据模型 + */ public NoteItemData getItemData() { return mItemData; } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..a924928 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -14,97 +14,149 @@ * limitations under the License. */ +// 包声明:归属小米便签的UI模块,负责便签应用的设置页面逻辑 package net.micode.notes.ui; +// 导入安卓账号相关类:用于Google账号的获取与管理 import android.accounts.Account; import android.accounts.AccountManager; +// 导入安卓ActionBar类:设置顶部导航栏(返回按钮) import android.app.ActionBar; +// 导入安卓对话框类:用于账号选择、更改确认等弹窗 import android.app.AlertDialog; +// 导入安卓广播接收器类:接收同步服务的状态广播 import android.content.BroadcastReceiver; +// 导入安卓内容值类:用于更新便签数据库的同步相关字段 import android.content.ContentValues; +// 导入安卓上下文类:提供应用运行环境 import android.content.Context; +// 导入安卓对话框点击监听类:处理弹窗选项的点击事件 import android.content.DialogInterface; +// 导入安卓意图类:用于页面跳转、广播过滤 import android.content.Intent; +// 导入安卓意图过滤器类:筛选需要接收的广播 import android.content.IntentFilter; +// 导入安卓共享偏好设置类:存储便签的偏好配置(同步账号、最后同步时间等) import android.content.SharedPreferences; +// 导入安卓Bundle类:保存Activity状态 import android.os.Bundle; +// 导入安卓偏好设置相关类:构建设置页面的UI组件 import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; +// 导入安卓文本工具类:处理字符串判空、相等判断 import android.text.TextUtils; +// 导入安卓日期格式化类:格式化最后同步时间 import android.text.format.DateFormat; +// 导入安卓布局填充类:加载自定义布局(设置页面header、对话框标题等) import android.view.LayoutInflater; +// 导入安卓菜单相关类:处理ActionBar的菜单点击 import android.view.Menu; import android.view.MenuItem; +// 导入安卓视图相关类:操作按钮、文本框等UI控件 import android.view.View; import android.widget.Button; import android.widget.TextView; +// 导入安卓吐司类:显示操作结果提示 import android.widget.Toast; +// 导入小米便签资源类:引用字符串、布局、偏好设置xml等资源 import net.micode.notes.R; +// 导入便签数据常量类:定义便签URI、字段等核心常量 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; +// 导入GTask同步服务类:处理便签与Google Task的同步逻辑 import net.micode.notes.gtask.remote.GTaskSyncService; - +/** + * 便签应用设置页面Activity + * 继承自安卓PreferenceActivity,核心职责: + * 1. 管理Google账号的绑定/切换/移除,支持便签与Google Task同步; + * 2. 提供手动触发同步、取消同步的按钮,展示同步状态和最后同步时间; + * 3. 存储/读取偏好设置(同步账号、最后同步时间、背景色设置等); + * 4. 接收同步服务的广播,实时刷新同步状态UI; + * 5. 处理设置页面的导航(返回便签列表页)。 + */ public class NotesPreferenceActivity extends PreferenceActivity { + /** 偏好设置文件名:所有便签偏好配置存储在该文件下 */ public static final String PREFERENCE_NAME = "notes_preferences"; - + /** 偏好设置Key:存储当前绑定的Google同步账号名 */ public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; - + /** 偏好设置Key:存储最后一次同步的时间戳 */ public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; - + /** 偏好设置Key:控制便签背景色是否随机显示 */ public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + /** 私有偏好设置Key:同步账号相关的偏好分类标识 */ private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; - + /** 私有常量:添加账号时的权限过滤Key */ private static final String AUTHORITIES_FILTER_KEY = "authorities"; + /** 账号相关的偏好分类组件:承载账号选择的偏好项 */ private PreferenceCategory mAccountCategory; - + /** 广播接收器:接收GTask同步服务的状态广播(同步中/同步完成) */ private GTaskReceiver mReceiver; - + /** 原始Google账号数组:用于对比是否新增了账号 */ private Account[] mOriAccounts; - + /** 标记位:是否触发了添加新账号的操作 */ private boolean mHasAddedAccount; + /** + * Activity创建时的初始化逻辑 + * 核心操作:设置ActionBar、加载偏好设置布局、初始化账号偏好组件、注册同步广播接收器、添加页面header + * @param icicle 保存Activity状态的Bundle + */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - /* using the app icon for navigation */ + /* 使用应用图标作为导航:开启ActionBar的返回按钮 */ getActionBar().setDisplayHomeAsUpEnabled(true); + // 从xml资源加载偏好设置页面的UI结构(res/xml/preferences.xml) addPreferencesFromResource(R.xml.preferences); + // 获取账号相关的偏好分类组件 mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + // 初始化同步广播接收器 mReceiver = new GTaskReceiver(); IntentFilter filter = new IntentFilter(); + // 过滤GTask同步服务的广播动作 filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + // 注册广播接收器,监听同步状态变化 registerReceiver(mReceiver, filter); + // 初始化原始账号数组为null mOriAccounts = null; + // 加载设置页面的header布局并添加到列表顶部 View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); getListView().addHeaderView(header, null, true); } + /** + * Activity恢复可见时的逻辑 + * 核心操作:检查是否新增了Google账号,自动绑定新账号;刷新设置页面UI + */ @Override protected void onResume() { super.onResume(); - // need to set sync account automatically if user has added a new - // account + // 若触发了添加账号操作,检查是否有新账号并自动绑定 if (mHasAddedAccount) { + // 获取当前设备的Google账号列表 Account[] accounts = getGoogleAccounts(); + // 对比原始账号数组,判断是否新增了账号 if (mOriAccounts != null && accounts.length > mOriAccounts.length) { for (Account accountNew : accounts) { boolean found = false; + // 遍历原始账号,判断新账号是否已存在 for (Account accountOld : mOriAccounts) { if (TextUtils.equals(accountOld.name, accountNew.name)) { found = true; break; } } + // 找到新增账号,自动绑定为同步账号 if (!found) { setSyncAccount(accountNew.name); break; @@ -113,36 +165,52 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + // 刷新设置页面的UI(账号偏好、同步按钮、同步状态) refreshUI(); } + /** + * Activity销毁时的清理逻辑 + * 核心操作:注销广播接收器,防止内存泄漏 + */ @Override protected void onDestroy() { + // 注销同步广播接收器 if (mReceiver != null) { unregisterReceiver(mReceiver); } super.onDestroy(); } + /** + * 加载账号偏好项 + * 核心逻辑:构建账号选择的偏好组件,设置点击事件(选择/更改账号) + */ private void loadAccountPreference() { + // 清空账号分类下的原有偏好项,避免重复添加 mAccountCategory.removeAll(); + // 创建账号选择的偏好项 Preference accountPref = new Preference(this); + // 获取当前绑定的默认同步账号 final String defaultAccount = getSyncAccountName(this); + // 设置偏好项标题和摘要 accountPref.setTitle(getString(R.string.preferences_account_title)); accountPref.setSummary(getString(R.string.preferences_account_summary)); + // 设置偏好项点击事件 accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { public boolean onPreferenceClick(Preference preference) { + // 同步中不允许更改账号 if (!GTaskSyncService.isSyncing()) { + // 未绑定账号:显示账号选择对话框 if (TextUtils.isEmpty(defaultAccount)) { - // the first time to set account showSelectAccountAlertDialog(); } else { - // if the account has already been set, we need to promp - // user about the risk + // 已绑定账号:显示更改账号的确认对话框(提示风险) showChangeAccountConfirmAlertDialog(); } } else { + // 同步中提示无法更改账号 Toast.makeText(NotesPreferenceActivity.this, R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) .show(); @@ -151,15 +219,25 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); + // 将账号偏好项添加到账号分类组件中 mAccountCategory.addPreference(accountPref); } + /** + * 加载同步按钮与同步状态 + * 核心逻辑: + * 1. 根据同步状态设置按钮文本(立即同步/取消同步)和点击事件; + * 2. 展示最后同步时间或同步中进度; + * 3. 无绑定账号时禁用同步按钮。 + */ private void loadSyncButton() { + // 获取同步按钮和最后同步时间文本框 Button syncButton = (Button) findViewById(R.id.preference_sync_button); TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); - // set button state + // 根据同步状态设置按钮状态 if (GTaskSyncService.isSyncing()) { + // 同步中:按钮文本设为“取消同步”,点击触发取消同步 syncButton.setText(getString(R.string.preferences_button_sync_cancel)); syncButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { @@ -167,6 +245,7 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); } else { + // 未同步:按钮文本设为“立即同步”,点击触发同步 syncButton.setText(getString(R.string.preferences_button_sync_immediately)); syncButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { @@ -174,33 +253,50 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); } + // 无绑定账号时禁用同步按钮 syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); - // set last sync time + // 设置最后同步时间/同步进度 if (GTaskSyncService.isSyncing()) { + // 同步中:显示同步进度文本 lastSyncTimeView.setText(GTaskSyncService.getProgressString()); lastSyncTimeView.setVisibility(View.VISIBLE); } else { + // 未同步:获取最后同步时间戳 long lastSyncTime = getLastSyncTime(this); if (lastSyncTime != 0) { + // 有同步记录:格式化并显示最后同步时间 lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, DateFormat.format(getString(R.string.preferences_last_sync_time_format), lastSyncTime))); lastSyncTimeView.setVisibility(View.VISIBLE); } else { + // 无同步记录:隐藏时间文本 lastSyncTimeView.setVisibility(View.GONE); } } } + /** + * 刷新设置页面UI + * 核心操作:重新加载账号偏好项和同步按钮/状态 + */ private void refreshUI() { loadAccountPreference(); loadSyncButton(); } + /** + * 显示账号选择对话框 + * 核心逻辑: + * 1. 展示当前设备的Google账号列表,支持选择绑定; + * 2. 提供“添加新账号”选项,跳转系统添加账号页面; + * 3. 选择账号后自动绑定为同步账号。 + */ private void showSelectAccountAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + // 加载自定义对话框标题布局 View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); @@ -208,25 +304,33 @@ public class NotesPreferenceActivity extends PreferenceActivity { subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); dialogBuilder.setCustomTitle(titleView); + // 隐藏默认的PositiveButton(通过单选列表选择账号) dialogBuilder.setPositiveButton(null, null); + // 获取设备的Google账号列表 Account[] accounts = getGoogleAccounts(); + // 获取当前绑定的账号 String defAccount = getSyncAccountName(this); + // 记录当前账号数组,用于后续对比是否新增账号 mOriAccounts = accounts; + // 初始化标记位:未添加新账号 mHasAddedAccount = false; + // 有Google账号时,构建单选列表 if (accounts.length > 0) { CharSequence[] items = new CharSequence[accounts.length]; final CharSequence[] itemMapping = items; int checkedItem = -1; int index = 0; + // 遍历账号,构建列表项并标记当前绑定账号 for (Account account : accounts) { if (TextUtils.equals(account.name, defAccount)) { checkedItem = index; } items[index++] = account.name; } + // 设置单选列表,选择后绑定账号并刷新UI dialogBuilder.setSingleChoiceItems(items, checkedItem, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -237,61 +341,99 @@ public class NotesPreferenceActivity extends PreferenceActivity { }); } + // 加载“添加新账号”的视图并添加到对话框 View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); dialogBuilder.setView(addAccountView); + // 显示对话框 final AlertDialog dialog = dialogBuilder.show(); + // 设置“添加新账号”点击事件:跳转系统添加账号页面 addAccountView.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { + // 标记触发了添加账号操作 mHasAddedAccount = true; Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + // 过滤仅显示Google账号添加选项 intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { "gmail-ls" }); + // 启动添加账号页面(无返回值) startActivityForResult(intent, -1); dialog.dismiss(); } }); } + /** + * 显示更改账号的确认对话框 + * 核心逻辑: + * 1. 提示更改账号的风险; + * 2. 提供“更改账号”“移除账号”“取消”三个选项; + * 3. 对应选项触发不同逻辑(选择新账号/移除当前账号)。 + */ private void showChangeAccountConfirmAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + // 加载自定义对话框标题布局 View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + // 标题显示当前绑定的账号名 titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, getSyncAccountName(this))); TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + // 副标题提示更改账号的风险 subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); dialogBuilder.setCustomTitle(titleView); + // 构建对话框选项:更改账号、移除账号、取消 CharSequence[] menuItemArray = new CharSequence[] { getString(R.string.preferences_menu_change_account), getString(R.string.preferences_menu_remove_account), getString(R.string.preferences_menu_cancel) }; + // 设置选项点击事件 dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { if (which == 0) { + // 选择“更改账号”:显示账号选择对话框 showSelectAccountAlertDialog(); } else if (which == 1) { + // 选择“移除账号”:清理绑定的账号信息 removeSyncAccount(); refreshUI(); } + // which==2为取消,无操作 } }); dialogBuilder.show(); } + /** + * 获取设备上的Google账号列表 + * @return Account[]:所有类型为“com.google”的账号 + */ private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); + // 根据账号类型筛选Google账号 return accountManager.getAccountsByType("com.google"); } + /** + * 设置同步账号 + * 核心逻辑: + * 1. 更新SharedPreferences存储的账号名; + * 2. 清理最后同步时间; + * 3. 异步清理本地便签的GTask同步相关字段(GTASK_ID、SYNC_ID); + * 4. 提示设置成功。 + * @param account 要绑定的Google账号名 + */ private void setSyncAccount(String account) { + // 账号未变化时不执行操作 if (!getSyncAccountName(this).equals(account)) { + // 获取偏好设置编辑器 SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); + // 存储新账号名(空账号则存空字符串) if (account != null) { editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); } else { @@ -299,37 +441,49 @@ public class NotesPreferenceActivity extends PreferenceActivity { } editor.commit(); - // clean up last sync time + // 清理最后同步时间 setLastSyncTime(this, 0); - // clean up local gtask related info + // 异步清理本地便签的GTask同步信息(避免阻塞主线程) new Thread(new Runnable() { public void run() { ContentValues values = new ContentValues(); + // 清空GTASK_ID和SYNC_ID values.put(NoteColumns.GTASK_ID, ""); values.put(NoteColumns.SYNC_ID, 0); + // 更新所有便签的同步字段 getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); } }).start(); + // 提示账号设置成功 Toast.makeText(NotesPreferenceActivity.this, getString(R.string.preferences_toast_success_set_accout, account), Toast.LENGTH_SHORT).show(); } } + /** + * 移除同步账号 + * 核心逻辑: + * 1. 从SharedPreferences中移除账号名和最后同步时间; + * 2. 异步清理本地便签的GTask同步相关字段; + */ private void removeSyncAccount() { + // 获取偏好设置编辑器 SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); + // 移除同步账号名 if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); } + // 移除最后同步时间 if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { editor.remove(PREFERENCE_LAST_SYNC_TIME); } editor.commit(); - // clean up local gtask related info + // 异步清理本地便签的GTask同步信息 new Thread(new Runnable() { public void run() { ContentValues values = new ContentValues(); @@ -340,12 +494,22 @@ public class NotesPreferenceActivity extends PreferenceActivity { }).start(); } + /** + * 静态工具方法:获取当前绑定的同步账号名 + * @param context 应用上下文 + * @return String:绑定的账号名(无则返回空字符串) + */ public static String getSyncAccountName(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); } + /** + * 静态工具方法:设置最后同步时间戳 + * @param context 应用上下文 + * @param time 同步时间戳(毫秒) + */ public static void setLastSyncTime(Context context, long time) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -354,17 +518,33 @@ public class NotesPreferenceActivity extends PreferenceActivity { editor.commit(); } + /** + * 静态工具方法:获取最后同步时间戳 + * @param context 应用上下文 + * @return long:最后同步时间戳(无则返回0) + */ public static long getLastSyncTime(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); } + /** + * 内部广播接收器类:接收GTask同步服务的状态广播 + * 核心职责:同步状态变化时刷新UI,展示同步进度 + */ private class GTaskReceiver extends BroadcastReceiver { + /** + * 接收广播时的处理逻辑 + * @param context 广播上下文 + * @param intent 携带同步状态的意图 + */ @Override public void onReceive(Context context, Intent intent) { + // 刷新设置页面UI refreshUI(); + // 同步中:更新同步进度文本 if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); syncStatus.setText(intent @@ -374,10 +554,18 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 处理ActionBar菜单点击事件 + * 核心逻辑:点击返回按钮时,跳转回便签列表页并清除顶部Activity栈 + * @param item 被点击的菜单项 + * @return boolean:是否处理了该点击事件 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: + // 构建跳转到便签列表页的意图 Intent intent = new Intent(this, NotesListActivity.class); + // 清除顶部Activity栈,避免返回时重复创建 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; @@ -385,4 +573,4 @@ public class NotesPreferenceActivity extends PreferenceActivity { return false; } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java index adcb2f7..1273c8c 100644 --- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java +++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -14,34 +14,70 @@ * 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; - +/** + * 2x尺寸便签桌面小部件提供者类 + * 继承自基础的NoteWidgetProvider,专门适配2x尺寸的小部件布局、背景和类型标识 + */ public class NoteWidgetProvider_2x extends NoteWidgetProvider { + /** + * 重写小部件更新方法 + * 触发2x尺寸小部件的更新逻辑,调用父类的update方法完成具体更新操作 + * @param context 应用上下文 + * @param appWidgetManager 桌面小部件管理器 + * @param appWidgetIds 需要更新的小部件ID数组 + */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的update方法,执行2x尺寸小部件的更新核心逻辑 super.update(context, appWidgetManager, appWidgetIds); } + /** + * 重写获取布局ID的方法 + * 返回2x尺寸小部件对应的布局资源ID + * @return 2x小部件布局资源ID + */ @Override protected int getLayoutId() { + // 返回R.layout.widget_2x:2x尺寸小部件的布局文件ID return R.layout.widget_2x; } + /** + * 重写获取背景资源ID的方法 + * 根据传入的背景ID,解析并返回2x尺寸小部件对应的背景资源ID + * @param bgId 背景标识ID + * @return 2x尺寸小部件对应的背景资源ID + */ @Override protected int getBgResourceId(int bgId) { + // 调用ResourceParser工具类,获取2x尺寸小部件对应的背景资源ID return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); } + /** + * 重写获取小部件类型的方法 + * 返回2x尺寸便签小部件的类型标识 + * @return 2x小部件类型常量(Notes.TYPE_WIDGET_2X) + */ @Override protected int getWidgetType() { + // 返回Notes.TYPE_WIDGET_2X:2x尺寸便签小部件的类型标识常量 return Notes.TYPE_WIDGET_2X; } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java index c12a02e..1b69129 100644 --- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java +++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -14,33 +14,70 @@ * 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; - +/** + * 4x尺寸便签桌面小部件提供者类 + * 继承自基础的NoteWidgetProvider抽象类,专门适配4x尺寸的桌面小部件, + * 核心职责是定义4x尺寸小部件的布局、背景资源和类型标识 + */ public class NoteWidgetProvider_4x extends NoteWidgetProvider { + /** + * 重写小部件更新回调方法 + * 当4x尺寸便签小部件需要更新时触发,调用父类update方法执行具体更新逻辑 + * @param context 应用上下文,用于资源访问和组件交互 + * @param appWidgetManager 小部件管理器,负责小部件的更新操作 + * @param appWidgetIds 需要更新的4x小部件ID数组(支持多实例) + */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类核心更新方法,复用通用更新逻辑,适配4x尺寸小部件 super.update(context, appWidgetManager, appWidgetIds); } + /** + * 定义4x尺寸小部件对应的布局资源ID + * (注:此处未显式标注@Override,但逻辑上是重写父类抽象方法) + * @return 4x小部件专属布局文件ID(R.layout.widget_4x) + */ protected int getLayoutId() { + // 返回widget_4x布局ID:该布局定义了4x尺寸小部件的UI结构 return R.layout.widget_4x; } + /** + * 重写背景资源获取方法 + * 根据背景标识ID,匹配并返回4x尺寸小部件对应的背景资源ID + * @param bgId 背景样式标识(不同数值对应不同背景风格) + * @return 4x尺寸小部件的背景资源ID + */ @Override protected int getBgResourceId(int bgId) { + // 调用工具类方法,获取4x尺寸小部件对应的背景资源ID return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); } + /** + * 重写小部件类型获取方法 + * 返回4x尺寸便签小部件的专属类型标识,用于区分不同尺寸的小部件 + * @return 4x小部件类型常量(Notes.TYPE_WIDGET_4X) + */ @Override protected int getWidgetType() { + // 返回Notes中定义的4x小部件类型常量,用于业务层识别小部件尺寸 return Notes.TYPE_WIDGET_4X; } -} +} \ No newline at end of file -- 2.34.1