diff --git a/doc/1.txt b/doc/1.txt deleted file mode 100644 index ecb1236..0000000 --- a/doc/1.txt +++ /dev/null @@ -1 +0,0 @@ -这个项目很有趣 diff --git a/doc/新模板泛读报告.docx b/doc/开源软件泛读、标注和维护报告文档.docx similarity index 96% rename from doc/新模板泛读报告.docx rename to doc/开源软件泛读、标注和维护报告文档.docx index cb7051f..50dff9a 100644 Binary files a/doc/新模板泛读报告.docx and b/doc/开源软件泛读、标注和维护报告文档.docx differ diff --git a/doc/第一周小米便签开源代码的泛读报告.docx b/doc/第一周小米便签开源代码的泛读报告.docx deleted file mode 100644 index 2238e73..0000000 Binary files a/doc/第一周小米便签开源代码的泛读报告.docx and /dev/null differ diff --git a/doc/第三周小米便签开源代码的泛读报告.docx b/doc/第三周小米便签开源代码的泛读报告.docx deleted file mode 100644 index d64747c..0000000 Binary files a/doc/第三周小米便签开源代码的泛读报告.docx and /dev/null differ diff --git a/doc/第二周小米便签开源代码的泛读报告.docx b/doc/第二周小米便签开源代码的泛读报告.docx deleted file mode 100644 index e6d4268..0000000 Binary files a/doc/第二周小米便签开源代码的泛读报告.docx and /dev/null differ diff --git a/src/src/net/micode/notes/model/Note.java b/src/src/net/micode/notes/model/Note.java index 6706cf6..0d9070b 100644 --- a/src/src/net/micode/notes/model/Note.java +++ b/src/src/net/micode/notes/model/Note.java @@ -1,20 +1,5 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package net.micode.notes.model; + import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentUris; @@ -33,32 +18,69 @@ import net.micode.notes.data.Notes.TextNote; import java.util.ArrayList; - +/** + * 便签实体类 (Model Layer) + *

+ * 设计模式:数据缓冲 (Data Buffering) + * 职责: + * 1. 作为内存数据与 SQLite 数据库之间的中间层。 + * 2. 维护“脏数据”状态 (Dirty State):仅缓存相对于数据库有变更的字段。 + * 3. 提供原子性的同步机制 (`syncNote`) 将变更写入 ContentProvider。 + *

+ * 结构说明: + * 小米便签底层数据库采用了“主表(Note)-附表(Data)”分离的设计。 + * 本类对应主表,内部类 {@link NoteData} 对应附表。 + */ public class Note { + /** + * 存储主表(Note表)的差异数据。 + * key: 数据库列名, value: 修改后的值。 + * 只有被修改过的字段才会存在于此,未修改字段不占用内存。 + */ private ContentValues mNoteDiffValues; + + /** + * 内部类代理,负责管理该便签关联的具体内容数据(如文本正文、通话记录信息)。 + */ private NoteData mNoteData; + private static final String TAG = "Note"; + /** - * Create a new note id for adding a new note to databases + * 静态工厂方法:在数据库中初始化一条全新的便签记录。 + *

+ * 这是一个同步方法 (synchronized),确保在高并发场景下(如批量导入或快速创建) + * 不会因资源竞争导致 ID 生成错误或数据库锁死。 + * + * @param context Android 上下文,用于获取 ContentResolver。 + * @param folderId 新便签归属的文件夹 ID。 + * @return 新创建的便签在数据库中的唯一行 ID (ROWID)。 + * @throws IllegalStateException 如果数据库插入失败或无法获取新 ID。 */ public static synchronized long getNewNoteId(Context context, long folderId) { - // Create a new note in the database ContentValues values = new ContentValues(); long createdTime = System.currentTimeMillis(); + + // 初始化元数据:创建时间、修改时间、便签类型 values.put(NoteColumns.CREATED_DATE, createdTime); values.put(NoteColumns.MODIFIED_DATE, createdTime); values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为 1,表示该数据需同步至云端 values.put(NoteColumns.PARENT_ID, folderId); + + // 执行数据库插入操作 Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); long noteId = 0; try { + // ContentProvider 插入成功后返回的 URI 格式通常为: content://.../note/123 + // 此处解析 URI 获取末尾的 ID 部分 noteId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { Log.e(TAG, "Get note id error :" + e.toString()); noteId = 0; } + if (noteId == -1) { throw new IllegalStateException("Wrong note id:" + noteId); } @@ -70,16 +92,28 @@ public class Note { mNoteData = new NoteData(); } + /** + * 设置便签的主表属性(元数据)。 + *

+ * 逻辑说明: + * 除了更新目标字段外,还会强制更新 {@link NoteColumns#LOCAL_MODIFIED} 和 {@link NoteColumns#MODIFIED_DATE}。 + * 这确保了任何属性的修改都会被标记为“脏数据”,从而在下次同步时被识别。 + * + * @param key 数据库列名 (e.g., NoteColumns.BG_COLOR_ID) + * @param value 要设置的值 + */ public void setNoteValue(String key, String value) { mNoteDiffValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + // 委托方法:将文本数据的设置操作转发给内部类处理 public void setTextData(String key, String value) { mNoteData.setTextData(key, value); } + // 委托方法:设置关联的文本数据行 ID public void setTextDataId(long id) { mNoteData.setTextDataId(id); } @@ -88,40 +122,67 @@ public class Note { return mNoteData.mTextDataId; } + // 委托方法:设置关联的通话记录数据行 ID public void setCallDataId(long id) { mNoteData.setCallDataId(id); } + // 委托方法:将通话记录数据的设置操作转发给内部类处理 public void setCallData(String key, String value) { mNoteData.setCallData(key, value); } + /** + * 检查当前内存对象是否包含未保存的修改。 + *

+ * 性能优化点: + * 在执行昂贵的 I/O 操作前调用此方法,如果返回 false,可直接跳过数据库写入。 + * + * @return true 表示有未提交的变更(主表或附表任一有变更)。 + */ public boolean isLocalModified() { return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); } + /** + * 将内存中的变更同步写入数据库 (Flush changes to DB)。 + *

+ * 逻辑流程: + * 1. 检查是否有变更,无变更则直接返回。 + * 2. 更新主表 (Note Table)。 + * 3. 更新附表 (Data Table)。 + * + * @param context 上下文 + * @param noteId 目标便签 ID + * @return true 表示同步成功(或无需同步),false 表示同步过程中出现严重错误。 + */ public boolean syncNote(Context context, long noteId) { if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); } + // 性能优化:无变更时不执行 I/O if (!isLocalModified()) { return true; } - /** - * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and - * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the - * note data info + /* + * 步骤 1: 更新主表 (Note Table) + * 即使主表更新失败(极少情况),为了数据安全性,依然尝试继续更新附表数据。 + * 这种容错机制防止了因为元数据错误导致用户正文丢失。 */ if (context.getContentResolver().update( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, null) == 0) { Log.e(TAG, "Update note error, should not happen"); - // Do not return, fall through } + // 清空主表差异缓存,防止重复提交 mNoteDiffValues.clear(); + /* + * 步骤 2: 更新附表 (Data Table) + * 如果附表有脏数据,调用 pushIntoContentResolver 执行具体的 SQL 操作。 + */ if (mNoteData.isLocalModified() && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { return false; @@ -130,14 +191,20 @@ public class Note { return true; } + /** + * 内部类:负责管理便签的“内容数据” (Data Table)。 + *

+ * 架构背景: + * 数据库设计采用 One-To-Many 关系(虽然业务上通常是一对一)。 + * 一个 Note ID 可以关联多行 Data 数据(一行存文本,一行存电话记录等)。 + * 此类封装了这种复杂的关联逻辑。 + */ private class NoteData { - private long mTextDataId; - - private ContentValues mTextDataValues; - - private long mCallDataId; + private long mTextDataId; // 文本内容在 data 表中的行 ID + private ContentValues mTextDataValues; // 文本内容的脏数据缓存 - private ContentValues mCallDataValues; + private long mCallDataId; // 通话记录在 data 表中的行 ID + private ContentValues mCallDataValues; // 通话记录的脏数据缓存 private static final String TAG = "NoteData"; @@ -148,6 +215,7 @@ public class Note { mCallDataId = 0; } + // 检查是否有任何内容数据被修改 boolean isLocalModified() { return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; } @@ -166,36 +234,51 @@ public class Note { mCallDataId = id; } + // 设置通话记录数据,同时会副作用触发主表的“已修改”标记 void setCallData(String key, String value) { mCallDataValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + // 设置文本数据,同时会副作用触发主表的“已修改”标记 void setTextData(String key, String value) { mTextDataValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 将附表变更推送到数据库。 + *

+ * 核心逻辑: + * 使用 {@link ContentProviderOperation} 构建批量操作。 + * 根据 ID 是否为 0 智能判断是执行 INSERT 还是 UPDATE。 + * + * @param context 上下文 + * @param noteId 关联的主表 ID + * @return 成功返回操作后的 URI,失败返回 null。 + */ Uri pushIntoContentResolver(Context context, long noteId) { - /** - * Check for safety - */ if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); } + // 操作队列,用于 applyBatch 事务提交 ArrayList operationList = new ArrayList(); ContentProviderOperation.Builder builder = null; + // --- 处理文本数据逻辑 --- if(mTextDataValues.size() > 0) { mTextDataValues.put(DataColumns.NOTE_ID, noteId); + if (mTextDataId == 0) { + // Case A: 新数据 -> 执行 INSERT mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues); try { + // 插入成功后,立即回填 ID,防止下次重复插入 setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); } catch (NumberFormatException e) { Log.e(TAG, "Insert new text data fail with noteId" + noteId); @@ -203,6 +286,7 @@ public class Note { return null; } } else { + // Case B: 已有数据 -> 执行 UPDATE builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mTextDataId)); builder.withValues(mTextDataValues); @@ -211,6 +295,7 @@ public class Note { mTextDataValues.clear(); } + // --- 处理通话记录数据逻辑 (同上) --- if(mCallDataValues.size() > 0) { mCallDataValues.put(DataColumns.NOTE_ID, noteId); if (mCallDataId == 0) { @@ -233,10 +318,12 @@ public class Note { mCallDataValues.clear(); } + // 执行批量事务提交 if (operationList.size() > 0) { try { ContentProviderResult[] results = context.getContentResolver().applyBatch( Notes.AUTHORITY, operationList); + // 只要操作成功执行,返回主表 URI return (results == null || results.length == 0 || results[0] == null) ? null : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); } catch (RemoteException e) { @@ -250,4 +337,4 @@ public class Note { return null; } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/model/WorkingNote.java b/src/src/net/micode/notes/model/WorkingNote.java index be081e4..e1bf532 100644 --- a/src/src/net/micode/notes/model/WorkingNote.java +++ b/src/src/net/micode/notes/model/WorkingNote.java @@ -30,38 +30,47 @@ import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.Notes.TextNote; import net.micode.notes.tool.ResourceParser.NoteBgResources; - - +import net.micode.notes.tool.ThreadExecutor; + +/** + * 业务逻辑核心类 (Business Logic Layer) + *

+ * 职责: + * 1. 作为 View (Activity) 和 Model (Note) 之间的桥梁 (ViewModel角色)。 + * 2. 维护当前便签的运行时状态(如内容、背景色、模式)。 + * 3. 负责数据的加载 (load) 和保存 (save) 逻辑调度。 + * 4. 监听设置变化并通知 UI 更新。 + *

+ * 关键重构: + * 2025年重构版本中,修复了 {@link #saveNote()} 在主线程执行 I/O 操作导致的 ANR 问题, + * 引入了 {@link ThreadExecutor} 进行异步保存。 + */ public class WorkingNote { - // Note for the working note + // 持有的底层数据模型,负责具体的数据库 Diff 缓存 private Note mNote; - // Note Id + // 便签的唯一 ID (数据库 row id) private long mNoteId; - // Note content + // 便签的正文内容 (运行时缓存) private String mContent; - // Note mode + // 便签模式 (0: 普通文本, 1: 清单模式) private int mMode; - private long mAlertDate; - - private long mModifiedDate; - - private int mBgColorId; - - private int mWidgetId; - - private int mWidgetType; - - private long mFolderId; - + private long mAlertDate; // 提醒时间 + private long mModifiedDate; // 修改时间 + private int mBgColorId; // 背景颜色 ID + private int mWidgetId; // 关联的桌面小部件 ID + private int mWidgetType; // 关联的桌面小部件类型 + private long mFolderId; // 所属文件夹 ID private Context mContext; private static final String TAG = "WorkingNote"; - private boolean mIsDeleted; + private boolean mIsDeleted; // 标记是否被删除 + // 监听器:用于通知 Activity 界面更新(如背景色改变时刷新 UI) private NoteSettingChangedListener mNoteSettingStatusListener; + // 数据库查询投影:定义了从 DATA 表中查询哪些列 public static final String[] DATA_PROJECTION = new String[] { DataColumns.ID, DataColumns.CONTENT, @@ -72,6 +81,7 @@ public class WorkingNote { DataColumns.DATA4, }; + // 数据库查询投影:定义了从 NOTE 主表中查询哪些列 public static final String[] NOTE_PROJECTION = new String[] { NoteColumns.PARENT_ID, NoteColumns.ALERTED_DATE, @@ -81,49 +91,45 @@ public class WorkingNote { NoteColumns.MODIFIED_DATE }; + // 索引常量,用于从 Cursor 中快速取值 private static final int DATA_ID_COLUMN = 0; - private static final int DATA_CONTENT_COLUMN = 1; - private static final int DATA_MIME_TYPE_COLUMN = 2; - private static final int DATA_MODE_COLUMN = 3; - private static final int NOTE_PARENT_ID_COLUMN = 0; - private static final int NOTE_ALERTED_DATE_COLUMN = 1; - private static final int NOTE_BG_COLOR_ID_COLUMN = 2; - private static final int NOTE_WIDGET_ID_COLUMN = 3; - private static final int NOTE_WIDGET_TYPE_COLUMN = 4; - private static final int NOTE_MODIFIED_DATE_COLUMN = 5; - // New note construct + // 构造函数:创建新便签 private WorkingNote(Context context, long folderId) { mContext = context; mAlertDate = 0; mModifiedDate = System.currentTimeMillis(); mFolderId = folderId; mNote = new Note(); - mNoteId = 0; + mNoteId = 0; // ID 为 0 表示尚未持久化到数据库 mIsDeleted = false; mMode = 0; mWidgetType = Notes.TYPE_WIDGET_INVALIDE; } - // Existing note construct + // 构造函数:加载已有便签 private WorkingNote(Context context, long noteId, long folderId) { mContext = context; mNoteId = noteId; mFolderId = folderId; mIsDeleted = false; mNote = new Note(); - loadNote(); + loadNote(); // 立即触发数据加载 } + /** + * 加载 Note 主表数据。 + * 这是一个同步操作,会查询 SQLite 数据库。 + */ private void loadNote() { Cursor cursor = mContext.getContentResolver().query( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, @@ -143,13 +149,17 @@ public class WorkingNote { Log.e(TAG, "No note with id:" + mNoteId); throw new IllegalArgumentException("Unable to find note with id " + mNoteId); } + // 加载完主表后,紧接着加载 Data 表内容 loadNoteData(); } + /** + * 加载 Data 表数据(正文、电话记录等)。 + */ private void loadNoteData() { Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { - String.valueOf(mNoteId) + String.valueOf(mNoteId) }, null); if (cursor != null) { @@ -157,10 +167,12 @@ public class WorkingNote { do { String type = cursor.getString(DATA_MIME_TYPE_COLUMN); if (DataConstants.NOTE.equals(type)) { + // 如果是普通文本便签 mContent = cursor.getString(DATA_CONTENT_COLUMN); mMode = cursor.getInt(DATA_MODE_COLUMN); mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); } else if (DataConstants.CALL_NOTE.equals(type)) { + // 如果是电话记录便签 mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); } else { Log.d(TAG, "Wrong note type with type:" + type); @@ -174,8 +186,11 @@ public class WorkingNote { } } + /** + * 工厂方法:创建一个空便签对象(尚未存库)。 + */ public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, - int widgetType, int defaultBgColorId) { + int widgetType, int defaultBgColorId) { WorkingNote note = new WorkingNote(context, folderId); note.setBgColorId(defaultBgColorId); note.setWidgetId(widgetId); @@ -183,12 +198,71 @@ public class WorkingNote { return note; } + /** + * 工厂方法:加载指定 ID 的便签。 + */ public static WorkingNote load(Context context, long id) { return new WorkingNote(context, id, 0); } + + /** + * 核心方法:保存便签。 + *

+ * 架构变更说明 (2025 Refactoring): + * 原逻辑在主线程直接执行数据库 I/O,导致 onPause 时 UI 卡顿。 + * 现已改造为使用 {@link ThreadExecutor} 将任务派发至单线程池执行。 + * + * @return 总是返回 true(表示保存任务已成功提交),不再等待实际写入结果。 + */ public synchronized boolean saveNote() { if (isWorthSaving()) { + + // ================================================================================= + // [新代码开始] 使用 ThreadExecutor 在后台线程执行保存,防止卡顿 + // ================================================================================= + final Context context = mContext; + + // 提交异步任务 + ThreadExecutor.getInstance().execute(new Runnable() { + @Override + public void run() { + // --- 以下代码全部在后台线程运行,绝对不会卡顿界面 --- + + // 1. 如果数据库里还没这条便签(是新建的),先去插入一条占位 + // 这里涉及 mNoteId 的修改,虽然是在后台,但因为此时 Activity 处于 onPause, + // 用户不太可能同时操作,所以相对安全。 + if (!existInDatabase()) { + long newId = Note.getNewNoteId(context, mFolderId); + if (newId == 0) { + Log.e(TAG, "Create new note fail"); + return; + } + mNoteId = newId; + } + + // 2. 执行真正的同步(最耗时的 update 操作) + // mNote.syncNote 会将内存中的 Diff 写入数据库 + mNote.syncNote(context, mNoteId); + + // 3. 更新桌面小部件 (如果关联了 Widget) + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + Log.d(TAG, "Note saved in background thread."); + } + }); + // [新代码结束] 直接返回 true,不等待数据库写入完成 + + + // ================================================================================= + // [旧代码备份 - 已注释] 原生逻辑,会在主线程执行导致 ANR + // ================================================================================= + /* + // 这里的符号 "/*" 表示注释开始,编译器会忽略下面的代码 + if (!existInDatabase()) { if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { Log.e(TAG, "Create new note fail with id:" + mNoteId); @@ -198,24 +272,38 @@ public class WorkingNote { mNote.syncNote(mContext, mNoteId); - /** - * Update widget content if there exist any widget of this note - */ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { mNoteSettingStatusListener.onWidgetChanged(); } return true; + + */ + // 这里的符号 "*/" 表示注释结束 + + return true; } else { return false; } } + + /** + * 判断便签是否已经存在于数据库中。 + * @return true 如果已有 ID (>0) + */ public boolean existInDatabase() { return mNoteId > 0; } + /** + * 校验便签是否值得保存。 + * 规则: + * 1. 已经被标记删除 -> 不保存 + * 2. 是新便签且内容为空 -> 不保存(避免产生垃圾空数据) + * 3. 是已有便签但没有修改过 -> 不保存(节省 I/O) + */ private boolean isWorthSaving() { if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) || (existInDatabase() && !mNote.isLocalModified())) { @@ -229,6 +317,7 @@ public class WorkingNote { mNoteSettingStatusListener = l; } + // 设置提醒时间,并通知 UI 更新闹钟图标 public void setAlertDate(long date, boolean set) { if (date != mAlertDate) { mAlertDate = date; @@ -243,10 +332,11 @@ public class WorkingNote { mIsDeleted = mark; if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); + mNoteSettingStatusListener.onWidgetChanged(); } } + // 设置背景色,并通知 UI 刷新背景 public void setBgColorId(int id) { if (id != mBgColorId) { mBgColorId = id; @@ -257,6 +347,7 @@ public class WorkingNote { } } + // 切换清单模式/普通模式 public void setCheckListMode(int mode) { if (mMode != mode) { if (mNoteSettingStatusListener != null) { @@ -281,6 +372,12 @@ public class WorkingNote { } } + /** + * 更新便签正文。 + *

+ * 注意:这里不仅更新了 WorkingNote 的缓存 {@link #mContent}, + * 也调用了 {@link Note#setTextData} 将变更记录到 Diff 中。 + */ public void setWorkingText(String text) { if (!TextUtils.equals(mContent, text)) { mContent = text; @@ -288,6 +385,7 @@ public class WorkingNote { } } + // 将普通便签转换为通话记录便签(特殊业务逻辑) public void convertToCallNote(String phoneNumber, long callDate) { mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); @@ -310,6 +408,7 @@ public class WorkingNote { return mModifiedDate; } + // 辅助方法:将颜色 ID 转换为资源 ID (R.drawable.xxx) public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); } @@ -342,27 +441,11 @@ public class WorkingNote { return mWidgetType; } + // 回调接口定义 public interface NoteSettingChangedListener { - /** - * Called when the background color of current note has just changed - */ void onBackgroundColorChanged(); - - /** - * Called when user set clock - */ void onClockAlertChanged(long date, boolean set); - - /** - * Call when user create note from widget - */ void onWidgetChanged(); - - /** - * Call when switch between check list mode and normal mode - * @param oldMode is previous mode before change - * @param newMode is new mode - */ void onCheckListModeChanged(int oldMode, int newMode); } } diff --git a/src/src/net/micode/notes/tool/BackupUtils.java b/src/src/net/micode/notes/tool/BackupUtils.java index 39f6ec4..a444b7b 100644 --- a/src/src/net/micode/notes/tool/BackupUtils.java +++ b/src/src/net/micode/notes/tool/BackupUtils.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.tool; @@ -36,11 +25,24 @@ import java.io.IOException; import java.io.PrintStream; +/** + * 备份工具类 (Tool Layer) + *

+ * 职责: + * 1. 负责将便签数据导出为本地文本文件 (TXT)。 + * 2. 检查外部存储 (SD Card) 的可用性。 + * 3. 封装了文件 I/O 流操作和异常处理。 + *

+ * 设计模式:单例模式 (Singleton),保证全局只有一个 BackupUtils 实例。 + */ public class BackupUtils { private static final String TAG = "BackupUtils"; - // Singleton stuff + // 单例实例引用 private static BackupUtils sInstance; + /** + * 获取单例实例 (线程安全) + */ public static synchronized BackupUtils getInstance(Context context) { if (sInstance == null) { sInstance = new BackupUtils(context); @@ -49,30 +51,38 @@ 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; private BackupUtils(Context context) { mTextExport = new TextExport(context); } + /** + * 检查外部存储 (SD Card) 是否已挂载且可读写。 + * 在执行任何文件操作前必须先调用此方法进行防御性检查。 + */ private static boolean externalStorageAvailable() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } + /** + * 执行导出操作。 + * @return 返回状态码 (STATE_*) + */ public int exportToText() { return mTextExport.exportToText(); } @@ -85,47 +95,50 @@ public class BackupUtils { return mTextExport.mFileDirectory; } + /** + * 内部类:文本导出器 + * 封装了从 ContentProvider 查询数据并格式化写入 PrintStream 的核心逻辑。 + */ private static class TextExport { + // 查询便签主表所需的列 private static final String[] NOTE_PROJECTION = { NoteColumns.ID, NoteColumns.MODIFIED_DATE, - NoteColumns.SNIPPET, + NoteColumns.SNIPPET, // 文件夹名称 NoteColumns.TYPE }; private static final int NOTE_COLUMN_ID = 0; - private static final int NOTE_COLUMN_MODIFIED_DATE = 1; - private static final int NOTE_COLUMN_SNIPPET = 2; + // 查询便签内容表所需的列 private static final String[] DATA_PROJECTION = { DataColumns.CONTENT, DataColumns.MIME_TYPE, - DataColumns.DATA1, + DataColumns.DATA1, // CALL_DATE DataColumns.DATA2, DataColumns.DATA3, - DataColumns.DATA4, + DataColumns.DATA4, // PHONE_NUMBER }; private static final int DATA_COLUMN_CONTENT = 0; - private static final int DATA_COLUMN_MIME_TYPE = 1; - private static final int DATA_COLUMN_CALL_DATE = 2; - private static final int DATA_COLUMN_PHONE_NUMBER = 4; + // 导出格式模板数组 (从资源文件读取) private final String [] TEXT_FORMAT; private static final int FORMAT_FOLDER_NAME = 0; private static final int FORMAT_NOTE_DATE = 1; private static final int FORMAT_NOTE_CONTENT = 2; private Context mContext; - private String mFileName; - private String mFileDirectory; + private String mFileName; // 导出的文件名 + private String mFileDirectory; // 导出的文件目录 public TextExport(Context context) { + // 从 strings.xml 中加载格式化字符串,例如 "Folder: %s" TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); mContext = context; mFileName = ""; @@ -137,23 +150,27 @@ public class BackupUtils { } /** - * Export the folder identified by folder id to text + * 导出指定文件夹下的所有便签。 + * + * @param folderId 文件夹 ID + * @param ps 目标文件输出流 */ private void exportFolderToText(String folderId, PrintStream ps) { - // Query notes belong to this folder + // 查询归属于该文件夹的所有便签 Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { - folderId + 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), 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()); @@ -163,38 +180,42 @@ public class BackupUtils { } /** - * Export note identified by id to a print stream + * 导出单条便签的详细内容 (可能包含文本或通话记录)。 */ private void exportNoteToText(String noteId, PrintStream ps) { Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { - noteId + noteId }, null); if (dataCursor != null) { if (dataCursor.moveToFirst()) { do { String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + + // 分支 A: 导出通话记录便签 if (DataConstants.CALL_NOTE.equals(mimeType)) { - // Print phone number String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); String location = dataCursor.getString(DATA_COLUMN_CONTENT); + // 写入电话号码 if (!TextUtils.isEmpty(phoneNumber)) { ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber)); } - // Print call date + // 写入通话日期 ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat .format(mContext.getString(R.string.format_datetime_mdhm), callDate))); - // Print call attachment location + // 写入归属地信息 if (!TextUtils.isEmpty(location)) { ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location)); } - } else if (DataConstants.NOTE.equals(mimeType)) { + } + // 分支 B: 导出普通文本便签 + 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), @@ -205,7 +226,8 @@ public class BackupUtils { } dataCursor.close(); } - // print a line separator between note + + // 在不同便签之间写入分隔符,便于阅读 try { ps.write(new byte[] { Character.LINE_SEPARATOR, Character.LETTER_NUMBER @@ -216,7 +238,14 @@ public class BackupUtils { } /** - * Note will be exported as text which is user readable + * 导出流程的主入口。 + *

+ * 逻辑流程: + * 1. 检查 SD 卡。 + * 2. 创建文件输出流。 + * 3. 遍历所有文件夹 -> 导出文件夹内的便签。 + * 4. 遍历根目录下的便签 -> 导出便签。 + * 5. 关闭流。 */ public int exportToText() { if (!externalStorageAvailable()) { @@ -229,7 +258,9 @@ public class BackupUtils { Log.e(TAG, "get print stream error"); return STATE_SYSTEM_ERROR; } - // First export folder and its notes + + // 第一步:导出所有文件夹及其包含的便签 + // 查询条件:(类型是文件夹 AND 不是回收站) OR (是通话记录文件夹) Cursor folderCursor = mContext.getContentResolver().query( Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, @@ -240,7 +271,7 @@ public class BackupUtils { if (folderCursor != null) { if (folderCursor.moveToFirst()) { do { - // Print folder's name + // 打印文件夹名称 String folderName = ""; if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { folderName = mContext.getString(R.string.call_record_folder_name); @@ -250,6 +281,7 @@ public class BackupUtils { 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,7 +289,7 @@ public class BackupUtils { folderCursor.close(); } - // Export notes in root's folder + // 第二步:导出根目录下的孤立便签 Cursor noteCursor = mContext.getContentResolver().query( Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, @@ -270,7 +302,7 @@ public class BackupUtils { 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()); @@ -283,7 +315,8 @@ public class BackupUtils { } /** - * Get a print stream pointed to the file {@generateExportedTextFile} + * 创建并打开导出文件的输出流。 + * 文件路径通常为 /sdcard/MIUI/notes/ */ private PrintStream getExportToTextPrintStream() { File file = generateFileMountedOnSDcard(mContext, R.string.file_path, @@ -310,13 +343,17 @@ public class BackupUtils { } /** - * Generate the text file to store imported data + * 在 SD 卡上创建目标文件。 + * 如果目录不存在,会自动创建目录。 + * 文件名通常包含当前日期时间,避免重名。 */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { StringBuilder sb = new StringBuilder(); + // 获取外部存储根路径 sb.append(Environment.getExternalStorageDirectory()); sb.append(context.getString(filePathResId)); File filedir = new File(sb.toString()); + // 格式化文件名:notes_20250103.txt sb.append(context.getString( fileNameFormatResId, DateFormat.format(context.getString(R.string.format_date_ymd), @@ -325,10 +362,10 @@ public class BackupUtils { try { if (!filedir.exists()) { - filedir.mkdir(); + filedir.mkdir(); // 创建目录 } if (!file.exists()) { - file.createNewFile(); + file.createNewFile(); // 创建空文件 } return file; } catch (SecurityException e) { @@ -339,6 +376,4 @@ public class BackupUtils { return null; } -} - - +} \ No newline at end of file diff --git a/src/src/net/micode/notes/tool/DataUtils.java b/src/src/net/micode/notes/tool/DataUtils.java index 2a14982..e9dde4b 100644 --- a/src/src/net/micode/notes/tool/DataUtils.java +++ b/src/src/net/micode/notes/tool/DataUtils.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.tool; @@ -34,9 +23,28 @@ import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import java.util.ArrayList; import java.util.HashSet; - +/** + * 数据工具类 (Tool Layer) + *

+ * 职责: + * 1. 提供针对数据库的批量操作接口 (Batch Operations)。 + * 2. 提供常用的数据查询与校验方法。 + * 3. 封装底层 ContentResolver 的复杂调用逻辑,向上层提供简洁的静态方法。 + */ public class DataUtils { public static final String TAG = "DataUtils"; + + /** + * 批量删除便签。 + *

+ * 逻辑说明: + * 使用 {@link ContentProviderOperation} 构建批量删除事务,确保操作的原子性。 + * 会自动跳过系统根文件夹 (ID_ROOT_FOLDER),防止核心数据被误删。 + * + * @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"); @@ -49,6 +57,7 @@ public class DataUtils { ArrayList operationList = new ArrayList(); for (long id : ids) { + // 防御性编程:禁止删除系统根目录 if(id == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Don't delete system folder root"); continue; @@ -72,6 +81,10 @@ public class DataUtils { return false; } + /** + * 将单个便签移动到指定文件夹。 + * 同时也记录了原文件夹 ID (ORIGIN_PARENT_ID),便于后续可能的撤销操作。 + */ public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, desFolderId); @@ -80,8 +93,12 @@ public class DataUtils { resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); } + /** + * 批量移动便签到指定文件夹。 + * 同样使用了 applyBatch 进行事务处理。 + */ public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, - long folderId) { + long folderId) { if (ids == null) { Log.d(TAG, "the ids is null"); return true; @@ -112,7 +129,8 @@ public class DataUtils { } /** - * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + * 获取用户创建的文件夹数量。 + * 排除系统文件夹 (TYPE_SYSTEM) 和回收站 (ID_TRASH_FOLDER)。 */ public static int getUserFolderCount(ContentResolver resolver) { Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, @@ -136,6 +154,11 @@ public class DataUtils { return count; } + /** + * 检查便签是否在数据库中可见(即未被彻底删除,且不在回收站中)。 + * + * @param type 便签类型 (TYPE_NOTE 或 TYPE_FOLDER) + */ public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, @@ -153,6 +176,10 @@ public class DataUtils { return exist; } + /** + * 检查主表 (Note Table) 中是否存在指定 ID 的记录。 + * 不考虑是否在回收站,只要 ID 存在即返回 true。 + */ public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, null, null, null); @@ -167,6 +194,9 @@ public class DataUtils { return exist; } + /** + * 检查附表 (Data Table) 中是否存在指定 ID 的记录。 + */ public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null, null, null, null); @@ -181,11 +211,15 @@ public class DataUtils { return exist; } + /** + * 检查是否存在同名的可见文件夹。 + * 用于防止创建重名文件夹。 + */ public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null, NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + - " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + - " AND " + NoteColumns.SNIPPET + "=?", + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.SNIPPET + "=?", new String[] { name }, null); boolean exist = false; if(cursor != null) { @@ -197,6 +231,10 @@ public class DataUtils { return exist; } + /** + * 获取指定文件夹下所有关联了桌面小部件 (Widget) 的便签信息。 + * 用于在删除或移动文件夹时,同时更新桌面的 Widget 状态。 + */ public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, @@ -224,6 +262,9 @@ public class DataUtils { return set; } + /** + * 根据便签 ID 查找其关联的电话号码 (仅针对 CallNote)。 + */ public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.PHONE_NUMBER }, @@ -243,11 +284,15 @@ public class DataUtils { return ""; } + /** + * 根据电话号码和通话时间反向查找便签 ID。 + * 使用了自定义的 SQL 函数 PHONE_NUMBERS_EQUAL 进行号码匹配。 + */ public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.NOTE_ID }, CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" - + CallNote.PHONE_NUMBER + ",?)", + + CallNote.PHONE_NUMBER + ",?)", new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, null); @@ -264,6 +309,10 @@ public class DataUtils { return 0; } + /** + * 获取便签的摘要信息 (Snippet)。 + * 在便签列表中,显示的标题其实就是 SNIPPET 字段。 + */ public static String getSnippetById(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, new String [] { NoteColumns.SNIPPET }, @@ -282,6 +331,10 @@ public class DataUtils { throw new IllegalArgumentException("Note is not found with id: " + noteId); } + /** + * 格式化摘要字符串。 + * 逻辑:如果摘要包含换行符,则只截取第一行作为标题。 + */ public static String getFormattedSnippet(String snippet) { if (snippet != null) { snippet = snippet.trim(); @@ -292,4 +345,4 @@ public class DataUtils { } return snippet; } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/tool/GTaskStringUtils.java b/src/src/net/micode/notes/tool/GTaskStringUtils.java index 666b729..4f009da 100644 --- a/src/src/net/micode/notes/tool/GTaskStringUtils.java +++ b/src/src/net/micode/notes/tool/GTaskStringUtils.java @@ -1,50 +1,65 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.tool; +/** + * GTask 字符串常量工具类 (Tool Layer) + *

+ * 职责: + * 1. 定义与 Google Tasks API 交互时所需的 JSON 字段键名 (Keys)。 + * 2. 定义同步操作的指令类型 (Action Types)。 + * 3. 定义小米便签在 GTask 中创建的特殊文件夹名称及元数据标识。 + *

+ * 说明:该类主要配合 {@link net.micode.notes.gtask.data} 包下的实体类使用, + * 用于解析服务器返回的 JSON 或构建发送给服务器的 JSON。 + */ public class GTaskStringUtils { + // ================================================================= + // GTask API 动作指令相关 (Action Operations) + // 用于在批量同步请求中标识具体的操作类型 + // ================================================================= + public final static String GTASK_JSON_ACTION_ID = "action_id"; public final static String GTASK_JSON_ACTION_LIST = "action_list"; public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + // 创建任务/列表指令 public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + // 获取所有数据指令 public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + // 移动任务指令 (改变父子关系或顺序) public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + // 更新任务内容指令 public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + + // ================================================================= + // GTask JSON 数据字段 Key 定义 (Data Fields) + // 对应 Google Tasks API 返回的 JSON 对象属性名 + // ================================================================= + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; - public final static String GTASK_JSON_COMPLETED = "completed"; + public final static String GTASK_JSON_COMPLETED = "completed"; // 完成时间 public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; - public final static String GTASK_JSON_DELETED = "deleted"; + public final static String GTASK_JSON_DELETED = "deleted"; // 是否已删除 public final static String GTASK_JSON_DEST_LIST = "dest_list"; @@ -52,17 +67,17 @@ public class GTaskStringUtils { public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; - public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; // 增量数据 - public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型 public final static String GTASK_JSON_GET_DELETED = "get_deleted"; - public final static String GTASK_JSON_ID = "id"; + public final static String GTASK_JSON_ID = "id"; // GTask 唯一 ID - public final static String GTASK_JSON_INDEX = "index"; + public final static String GTASK_JSON_INDEX = "index"; // 排序索引 - public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间 public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; @@ -70,15 +85,15 @@ public class GTaskStringUtils { public final static String GTASK_JSON_LISTS = "lists"; - public final static String GTASK_JSON_NAME = "name"; + public final static String GTASK_JSON_NAME = "name"; // 任务标题/列表名称 public final static String GTASK_JSON_NEW_ID = "new_id"; - public final static String GTASK_JSON_NOTES = "notes"; + public final static String GTASK_JSON_NOTES = "notes"; // 任务备注/描述 (这里存储便签正文) public final static String GTASK_JSON_PARENT_ID = "parent_id"; - public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; // 前一个兄弟节点 ID public final static String GTASK_JSON_RESULTS = "results"; @@ -88,17 +103,30 @@ public class GTaskStringUtils { public final static String GTASK_JSON_TYPE = "type"; - public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; // 列表类型 - public final static String GTASK_JSON_TYPE_TASK = "TASK"; + public final static String GTASK_JSON_TYPE_TASK = "TASK"; // 任务类型 public final static String GTASK_JSON_USER = "user"; + + // ================================================================= + // 小米便签特定标识 (MIUI Specific) + // 用于在 Google Tasks 中隔离和识别属于小米便签的数据 + // ================================================================= + + // 文件夹名称前缀,用于识别小米便签创建的列表 public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; public final static String FOLDER_DEFAULT = "Default"; - public final static String FOLDER_CALL_NOTE = "Call_Note"; + public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话记录专用文件夹 + + + // ================================================================= + // 同步元数据标识 (Metadata) + // 用于在 GTask 的备注字段中存储 JSON 格式的元信息 + // ================================================================= public final static String FOLDER_META = "METADATA"; @@ -108,6 +136,7 @@ public class GTaskStringUtils { public final static String META_HEAD_DATA = "meta_data"; + // 特殊的元数据任务名称,警告用户不要删除 public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/tool/ResourceParser.java b/src/src/net/micode/notes/tool/ResourceParser.java index 1ad3ad6..fb81ce7 100644 --- a/src/src/net/micode/notes/tool/ResourceParser.java +++ b/src/src/net/micode/notes/tool/ResourceParser.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.tool; @@ -22,8 +11,21 @@ import android.preference.PreferenceManager; import net.micode.notes.R; import net.micode.notes.ui.NotesPreferenceActivity; +/** + * 资源解析工具类 (Tool Layer / UI Helper) + *

+ * 职责: + * 1. 维护内部状态 ID (如颜色 ID、字体 ID) 与 Android 资源 ID (R.drawable.xxx) 的映射关系。 + * 2. 提供静态查找表,避免在 UI 代码中编写大量的 if-else 或 switch-case。 + * 3. 集中管理 UI 样式资源,便于更换主题。 + */ public class ResourceParser { + // ================================================================= + // 内部状态 ID 定义 (对应数据库存储的值) + // ================================================================= + + // 背景颜色 ID 常量 public static final int YELLOW = 0; public static final int BLUE = 1; public static final int WHITE = 2; @@ -32,6 +34,7 @@ public class ResourceParser { public static final int BG_DEFAULT_COLOR = YELLOW; + // 字体大小 ID 常量 public static final int TEXT_SMALL = 0; public static final int TEXT_MEDIUM = 1; public static final int TEXT_LARGE = 2; @@ -39,32 +42,50 @@ public class ResourceParser { public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + /** + * 内部类:编辑页面 (NoteEditActivity) 的背景资源管理 + */ public static class NoteBgResources { + // 编辑区正文背景图数组 private final static int [] BG_EDIT_RESOURCES = new int [] { - R.drawable.edit_yellow, - R.drawable.edit_blue, - R.drawable.edit_white, - R.drawable.edit_green, - R.drawable.edit_red + R.drawable.edit_yellow, + R.drawable.edit_blue, + R.drawable.edit_white, + R.drawable.edit_green, + R.drawable.edit_red }; + // 编辑区标题栏背景图数组 private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { - R.drawable.edit_title_yellow, - R.drawable.edit_title_blue, - R.drawable.edit_title_white, - R.drawable.edit_title_green, - R.drawable.edit_title_red + R.drawable.edit_title_yellow, + R.drawable.edit_title_blue, + R.drawable.edit_title_white, + R.drawable.edit_title_green, + R.drawable.edit_title_red }; + /** + * 根据颜色 ID 获取编辑页背景 Drawable ID + */ public static int getNoteBgResource(int id) { return BG_EDIT_RESOURCES[id]; } + /** + * 根据颜色 ID 获取编辑页标题栏 Drawable ID + */ public static int getNoteTitleBgResource(int id) { return BG_EDIT_TITLE_RESOURCES[id]; } } + /** + * 获取新建便签时的默认背景颜色 ID。 + *

+ * 逻辑: + * 检查用户设置 (SharedPreferences),如果开启了“随机背景色”,则返回一个随机 ID; + * 否则返回默认的黄色 (YELLOW)。 + */ public static int getDefaultBgId(Context context) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { @@ -74,37 +95,51 @@ public class ResourceParser { } } + /** + * 内部类:列表页面 (NotesListActivity) 的背景资源管理 + *

+ * 逻辑说明: + * 为了实现类似 iOS 的“分组圆角列表”效果,背景图被分为了 4 种状态: + * 1. First: 列表第一项 (顶部圆角) + * 2. Normal: 中间项 (无圆角) + * 3. Last: 列表最后一项 (底部圆角) + * 4. Single: 列表中只有一项 (全圆角) + */ public static class NoteItemBgResources { + // 顶部圆角背景 private final static int [] BG_FIRST_RESOURCES = new int [] { - R.drawable.list_yellow_up, - R.drawable.list_blue_up, - R.drawable.list_white_up, - R.drawable.list_green_up, - R.drawable.list_red_up + R.drawable.list_yellow_up, + R.drawable.list_blue_up, + R.drawable.list_white_up, + R.drawable.list_green_up, + R.drawable.list_red_up }; + // 无圆角背景 (中间项) private final static int [] BG_NORMAL_RESOURCES = new int [] { - R.drawable.list_yellow_middle, - R.drawable.list_blue_middle, - R.drawable.list_white_middle, - R.drawable.list_green_middle, - R.drawable.list_red_middle + R.drawable.list_yellow_middle, + R.drawable.list_blue_middle, + R.drawable.list_white_middle, + R.drawable.list_green_middle, + R.drawable.list_red_middle }; + // 底部圆角背景 private final static int [] BG_LAST_RESOURCES = new int [] { - R.drawable.list_yellow_down, - R.drawable.list_blue_down, - R.drawable.list_white_down, - R.drawable.list_green_down, - R.drawable.list_red_down, + R.drawable.list_yellow_down, + R.drawable.list_blue_down, + R.drawable.list_white_down, + R.drawable.list_green_down, + R.drawable.list_red_down, }; + // 全圆角背景 (单项) private final static int [] BG_SINGLE_RESOURCES = new int [] { - R.drawable.list_yellow_single, - R.drawable.list_blue_single, - R.drawable.list_white_single, - R.drawable.list_green_single, - R.drawable.list_red_single + R.drawable.list_yellow_single, + R.drawable.list_blue_single, + R.drawable.list_white_single, + R.drawable.list_green_single, + R.drawable.list_red_single }; public static int getNoteBgFirstRes(int id) { @@ -128,13 +163,17 @@ public class ResourceParser { } } + /** + * 内部类:桌面小部件 (Widget) 的背景资源管理 + * 包含 2x2 和 4x4 两种规格的背景图。 + */ public static class WidgetBgResources { private final static int [] BG_2X_RESOURCES = new int [] { - R.drawable.widget_2x_yellow, - R.drawable.widget_2x_blue, - R.drawable.widget_2x_white, - R.drawable.widget_2x_green, - R.drawable.widget_2x_red, + R.drawable.widget_2x_yellow, + R.drawable.widget_2x_blue, + R.drawable.widget_2x_white, + R.drawable.widget_2x_green, + R.drawable.widget_2x_red, }; public static int getWidget2xBgResource(int id) { @@ -142,11 +181,11 @@ public class ResourceParser { } private final static int [] BG_4X_RESOURCES = new int [] { - R.drawable.widget_4x_yellow, - R.drawable.widget_4x_blue, - R.drawable.widget_4x_white, - R.drawable.widget_4x_green, - R.drawable.widget_4x_red + R.drawable.widget_4x_yellow, + R.drawable.widget_4x_blue, + R.drawable.widget_4x_white, + R.drawable.widget_4x_green, + R.drawable.widget_4x_red }; public static int getWidget4xBgResource(int id) { @@ -154,19 +193,23 @@ public class ResourceParser { } } + /** + * 内部类:字体样式资源管理 + * 映射字体 ID 到 styles.xml 中的 TextAppearance 样式。 + */ public static class TextAppearanceResources { private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { - R.style.TextAppearanceNormal, - R.style.TextAppearanceMedium, - R.style.TextAppearanceLarge, - R.style.TextAppearanceSuper + R.style.TextAppearanceNormal, + R.style.TextAppearanceMedium, + R.style.TextAppearanceLarge, + R.style.TextAppearanceSuper }; 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} + * 容错处理 (Hack/Workaround): + * 防止 SharedPreference 中存储的字体 ID 超出资源数组的范围。 + * 如果 ID 非法 (>= 数组长度),强制返回默认的中号字体。 */ if (id >= TEXTAPPEARANCE_RESOURCES.length) { return BG_DEFAULT_FONT_SIZE; @@ -178,4 +221,4 @@ public class ResourceParser { return TEXTAPPEARANCE_RESOURCES.length; } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..24a9bf5 100644 --- a/src/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -39,21 +28,34 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; - +/** + * 闹钟弹窗 Activity (UI Layer) + *

+ * 职责: + * 1. 当定时提醒触发时启动,负责弹出对话框提醒用户。 + * 2. 播放闹钟铃声。 + * 3. 处理屏幕唤醒逻辑 (即使手机处于锁屏或休眠状态也能显示)。 + * 4. 提供入口让用户点击进入便签查看详情。 + *

+ * 交互模式:这是一个 Dialog 样式的 Activity (在 Manifest 中配置), + * 实现 OnClickListener 和 OnDismissListener 来处理对话框交互。 + */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { - private long mNoteId; - private String mSnippet; - private static final int SNIPPET_PREW_MAX_LEN = 60; - MediaPlayer mPlayer; + private long mNoteId; // 关联的便签 ID + private String mSnippet; // 便签摘要内容 + private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要截取最大长度 + MediaPlayer mPlayer; // 用于播放闹钟声音 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); + // 设置窗口 Flag:允许在锁屏界面之上显示 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 @@ -64,7 +66,11 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD Intent intent = getIntent(); try { + // 从 Intent 的 Data URI 中解析便签 ID + // 格式通常为: content://.../note/123 mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + + // 获取并处理便签摘要,过长则截断 mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) @@ -75,25 +81,36 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } mPlayer = new MediaPlayer(); + // 检查数据库中该便签是否还存在 (防止用户在闹钟响之前已经删除了便签) if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { - showActionDialog(); - playAlarmSound(); + showActionDialog(); // 显示弹窗 + playAlarmSound(); // 播放声音 } else { finish(); } } + /** + * 检查屏幕是否处于点亮状态 + */ private boolean isScreenOn() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); return pm.isScreenOn(); } + /** + * 播放闹钟音效 + * 使用系统默认的闹钟铃声,并根据系统静音设置调整音量流类型。 + */ 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 { @@ -102,7 +119,7 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD try { mPlayer.setDataSource(this, url); mPlayer.prepare(); - mPlayer.setLooping(true); + mPlayer.setLooping(true); // 循环播放 mPlayer.start(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block @@ -119,20 +136,31 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } } + /** + * 构建并显示提醒对话框 + */ 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); + + // "进入" 按钮:跳转到便签编辑页 + // 逻辑细节:只有当屏幕原本就是亮着的时候,才显示这个按钮。 + // 如果屏幕是黑的(被闹钟唤醒),可能用户只想看一眼,不一定要解锁进去编辑。 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); } dialog.show().setOnDismissListener(this); } + /** + * 处理对话框按钮点击事件 + */ public void onClick(DialogInterface dialog, int which) { switch (which) { - case DialogInterface.BUTTON_NEGATIVE: + case DialogInterface.BUTTON_NEGATIVE: // 点击了 "进入" Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.putExtra(Intent.EXTRA_UID, mNoteId); @@ -143,11 +171,18 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } } + /** + * 当对话框消失 (Dismiss) 时触发 + * 无论是点击按钮还是按返回键,都会触发此回调。 + */ public void onDismiss(DialogInterface dialog) { stopAlarmSound(); - finish(); + finish(); // 关闭 Activity } + /** + * 停止播放声音并释放资源 + */ private void stopAlarmSound() { if (mPlayer != null) { mPlayer.stop(); @@ -155,4 +190,4 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD mPlayer = null; } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/AlarmReceiver.java b/src/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..4f667dd 100644 --- a/src/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/src/net/micode/notes/ui/AlarmReceiver.java @@ -1,30 +1,78 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; +import android.app.AlarmManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +/** + * 闹钟初始化广播接收器 (System Integration Layer) + *

+ * 职责: + * 1. 监听系统启动完成广播 (BOOT_COMPLETED)。 + * 2. 扫描数据库中所有“未来需要提醒”的便签。 + * 3. 将这些提醒重新注册到 Android 系统 AlarmManager 中。 + *

+ * 背景知识: + * Android 的 AlarmManager 注册的闹钟在设备重启后会丢失(非持久化)。 + * 因此必须通过此类在开机时重建闹钟队列,保证提醒功能不会因为重启而失效。 + */ +public class AlarmInitReceiver extends BroadcastReceiver { + + // 数据库查询投影:仅查询 ID 和 提醒时间,节省内存 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; -public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - intent.setClass(context, AlarmAlertActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); + long currentDate = System.currentTimeMillis(); + + // 查询数据库:查找所有提醒时间晚于当前时间 (ALERTED_DATE > now) 的便签 + // 即只恢复那些“还没过期”的闹钟,过期的就不管了 + 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); + + if (c != null) { + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + + // 构建发送给 AlarmReceiver 的 Intent + // 这里必须与设置闹钟时的 Intent 结构完全一致,否则无法触发目标逻辑 + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统 AlarmManager 服务 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + + // 重新设定闹钟 + // RTC_WAKEUP: 使用绝对时间,并在触发时唤醒设备(如果设备处于休眠状态) + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + c.close(); + } } } diff --git a/src/src/net/micode/notes/ui/DateTimePicker.java b/src/src/net/micode/notes/ui/DateTimePicker.java index 496b0cd..795a580 100644 --- a/src/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/src/net/micode/notes/ui/DateTimePicker.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -28,13 +17,29 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; +/** + * 自定义日期时间选择器 (UI Component) + *

+ * 职责: + * 1. 提供一个组合控件,允许用户同时选择日期、小时、分钟。 + * 2. 处理 12小时制/24小时制 的显示逻辑切换。 + * 3. 实现时间滚轮的级联进位逻辑(分钟进位->小时,小时进位->日期)。 + *

+ * 结构: + * 继承自 FrameLayout,内部包含 4 个 NumberPicker: + * - Date (日期) + * - Hour (小时) + * - Minute (分钟) + * - AmPm (上午/下午) + */ public class DateTimePicker extends FrameLayout { private static final boolean DEFAULT_ENABLE_STATE = true; + // 常量定义:时间单位换算与滚轮范围 private static final int HOURS_IN_HALF_DAY = 12; private static final int HOURS_IN_ALL_DAY = 24; - private static final int DAYS_IN_ALL_WEEK = 7; + private static final int DAYS_IN_ALL_WEEK = 7; // 日期滚轮显示的跨度(显示前后7天) private static final int DATE_SPINNER_MIN_VAL = 0; private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; @@ -46,54 +51,71 @@ public class DateTimePicker extends FrameLayout { private static final int AMPM_SPINNER_MIN_VAL = 0; private static final int AMPM_SPINNER_MAX_VAL = 1; + // UI 控件引用 private final NumberPicker mDateSpinner; private final NumberPicker mHourSpinner; private final NumberPicker mMinuteSpinner; private final NumberPicker mAmPmSpinner; + + // 当前选中的时间状态 private Calendar mDate; + // 用于日期滚轮显示的字符串数组 (如 "12.05 Monday") private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; - private boolean mIsAm; - - private boolean mIs24HourView; - + private boolean mIsAm; // 是否为上午 + private boolean mIs24HourView; // 是否为24小时制 private boolean mIsEnabled = DEFAULT_ENABLE_STATE; - private boolean mInitialising; private OnDateTimeChangedListener mOnDateTimeChangedListener; + /** + * 监听器:日期滚轮变化 + * 逻辑:当用户滚动日期时,直接修改 Calendar 的天数,并刷新显示字符串。 + */ 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(); + updateDateControl(); // 重新计算滚轮上的日期文字,保持选中项居中 onDateTimeChanged(); } }; + /** + * 监听器:小时滚轮变化 + * 逻辑: + * 1. 检测跨午夜/跨中午的操作,进而调整日期 (DAY_OF_YEAR) 或 AM/PM 状态。 + * 2. 在 12 小时制下,11->12 或 12->11 需要特殊处理 AM/PM 切换。 + * 3. 在 24 小时制下,23->0 或 0->23 需要特殊处理 日期 加减。 + */ private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { boolean isDateChanged = false; Calendar cal = Calendar.getInstance(); if (!mIs24HourView) { + // 12小时制处理逻辑 if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + // 下午 11点 -> 12点 (跨越午夜,日期+1) 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) { + // 上午 12点 -> 11点 (倒退跨越午夜,日期-1) cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } + // 处理 AM/PM 切换 (11点与12点之间的边界) 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(); } } else { + // 24小时制处理逻辑:跨越 23->0 if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); @@ -104,6 +126,7 @@ public class DateTimePicker extends FrameLayout { isDateChanged = true; } } + // 更新 Calendar 对象 int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); mDate.set(Calendar.HOUR_OF_DAY, newHour); onDateTimeChanged(); @@ -115,22 +138,29 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 监听器:分钟滚轮变化 + * 逻辑:检测分钟进位 (59->0) 或借位 (0->59),从而联动调整小时 (Hour)。 + */ 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; + // 检测进位或借位 if (oldVal == maxValue && newVal == minValue) { offset += 1; } else if (oldVal == minValue && newVal == maxValue) { offset -= 1; } if (offset != 0) { + // 调整小时 mDate.add(Calendar.HOUR_OF_DAY, offset); mHourSpinner.setValue(getCurrentHour()); - updateDateControl(); + updateDateControl(); // 如果小时导致日期变更,需刷新日期控件 int newHour = getCurrentHourOfDay(); + // 调整 AM/PM 状态 if (newHour >= HOURS_IN_HALF_DAY) { mIsAm = false; updateAmPmControl(); @@ -144,6 +174,10 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 监听器:AM/PM 滚轮变化 + * 逻辑:切换上下午,同时调整 Calendar 的 HOUR_OF_DAY (+/- 12小时)。 + */ private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { @@ -160,7 +194,7 @@ public class DateTimePicker extends FrameLayout { public interface OnDateTimeChangedListener { void onDateTimeChanged(DateTimePicker view, int year, int month, - int dayOfMonth, int hourOfDay, int minute); + int dayOfMonth, int hourOfDay, int minute); } public DateTimePicker(Context context) { @@ -178,6 +212,7 @@ public class DateTimePicker extends FrameLayout { mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; inflate(context, R.layout.datetime_picker, this); + // 初始化各个 NumberPicker 并绑定监听器 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); @@ -188,7 +223,7 @@ public class DateTimePicker extends FrameLayout { mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); - mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnLongPressUpdateInterval(100); // 长按快速滚动 mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); @@ -198,19 +233,15 @@ public class DateTimePicker extends FrameLayout { mAmPmSpinner.setDisplayedValues(stringsForAmPm); mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - // update controls to initial state + // 更新控件状态以匹配当前时间 updateDateControl(); updateHourControl(); updateAmPmControl(); set24HourView(is24HourView); - - // set to current time setCurrentDate(date); - setEnabled(isEnabled()); - // set the content descriptions mInitialising = false; } @@ -232,20 +263,10 @@ public class DateTimePicker extends FrameLayout { return mIsEnabled; } - /** - * Get the current date in millis - * - * @return the current date in millis - */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } - /** - * Set the current date - * - * @param date The current date in millis - */ public void setCurrentDate(long date) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(date); @@ -253,17 +274,8 @@ public class DateTimePicker extends FrameLayout { 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 - */ public void setCurrentDate(int year, int month, - int dayOfMonth, int hourOfDay, int minute) { + int dayOfMonth, int hourOfDay, int minute) { setCurrentYear(year); setCurrentMonth(month); setCurrentDay(dayOfMonth); @@ -271,20 +283,10 @@ public class DateTimePicker extends FrameLayout { setCurrentMinute(minute); } - /** - * Get current year - * - * @return The current year - */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); } - /** - * Set current year - * - * @param year The current year - */ public void setCurrentYear(int year) { if (!mInitialising && year == getCurrentYear()) { return; @@ -294,20 +296,10 @@ public class DateTimePicker extends FrameLayout { onDateTimeChanged(); } - /** - * Get current month in the year - * - * @return The current month in the year - */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); } - /** - * Set current month in the year - * - * @param month The month in the year - */ public void setCurrentMonth(int month) { if (!mInitialising && month == getCurrentMonth()) { return; @@ -317,20 +309,10 @@ public class DateTimePicker extends FrameLayout { onDateTimeChanged(); } - /** - * Get current day of the month - * - * @return The day of the month - */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } - /** - * Set current day of the month - * - * @param dayOfMonth The day of the month - */ public void setCurrentDay(int dayOfMonth) { if (!mInitialising && dayOfMonth == getCurrentDay()) { return; @@ -340,14 +322,11 @@ public class DateTimePicker extends FrameLayout { onDateTimeChanged(); } - /** - * Get current hour in 24 hour mode, in the range (0~23) - * @return The current hour in 24 hour mode - */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } + // 获取用于显示的小时数 (12小时制或24小时制) private int getCurrentHour() { if (mIs24HourView){ return getCurrentHourOfDay(); @@ -362,9 +341,7 @@ public class DateTimePicker extends FrameLayout { } /** - * Set current hour in 24 hour mode, in the range (0~23) - * - * @param hourOfDay + * 设置小时,自动处理 12/24 转换及 AM/PM 状态更新 */ public void setCurrentHour(int hourOfDay) { if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { @@ -389,18 +366,10 @@ public class DateTimePicker extends FrameLayout { onDateTimeChanged(); } - /** - * Get currentMinute - * - * @return The Current Minute - */ public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); } - /** - * Set current minute - */ public void setCurrentMinute(int minute) { if (!mInitialising && minute == getCurrentMinute()) { return; @@ -410,17 +379,13 @@ public class DateTimePicker extends FrameLayout { onDateTimeChanged(); } - /** - * @return true if this is in 24 hour view else false. - */ 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. + * 切换 12/24 小时制 + * 自动隐藏或显示 AM/PM 选择器,并刷新小时滚轮的范围。 */ public void set24HourView(boolean is24HourView) { if (mIs24HourView == is24HourView) { @@ -434,16 +399,26 @@ public class DateTimePicker extends FrameLayout { updateAmPmControl(); } + /** + * 核心方法:更新日期滚轮的显示内容 + * + * 这个日期选择器不是无限滚动的日历,而是一个基于当前日期的"窗口"。 + * 它总是重新计算 `mDateDisplayValues` 数组,将当前选中日期放在滚轮的中间位置, + * 前后分别填充过去和未来的几天。 + */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(mDate.getTimeInMillis()); + // 回退 3 天 (DAYS_IN_ALL_WEEK / 2),准备从前3天开始填充数组 cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); mDateSpinner.setDisplayedValues(null); for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { cal.add(Calendar.DAY_OF_YEAR, 1); + // 格式化日期字符串,例如 "12.05 Monday" mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); } mDateSpinner.setDisplayedValues(mDateDisplayValues); + // 将选中项重置为中间索引 (3) mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); mDateSpinner.invalidate(); } @@ -468,10 +443,6 @@ public class DateTimePicker extends FrameLayout { } } - /** - * Set the callback that indicates the 'Set' button has been pressed. - * @param callback the callback, if null will do nothing - */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { mOnDateTimeChangedListener = callback; } @@ -482,4 +453,4 @@ public class DateTimePicker extends FrameLayout { getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..fb5be10 100644 --- a/src/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -29,37 +18,61 @@ import android.content.DialogInterface.OnClickListener; import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * 日期时间选择对话框 (UI Component Wrapper) + *

+ * 职责: + * 1. 封装 {@link DateTimePicker} 自定义控件,使其以弹窗形式展示。 + * 2. 管理对话框的生命周期和按钮事件 (确定/取消)。 + * 3. 实时联动:当用户滚动内部的滚轮时,动态更新对话框的标题显示当前选择的时间。 + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { - private Calendar mDate = Calendar.getInstance(); + private Calendar mDate = Calendar.getInstance(); // 当前选中的时间状态 private boolean mIs24HourView; private OnDateTimeSetListener mOnDateTimeSetListener; - private DateTimePicker mDateTimePicker; + private DateTimePicker mDateTimePicker; // 内部嵌入的自定义 View + /** + * 回调接口:当用户点击“确定”按钮时触发 + */ public interface OnDateTimeSetListener { void OnDateTimeSet(AlertDialog dialog, long date); } public DateTimePickerDialog(Context context, long date) { super(context); + // 1. 初始化内部自定义控件 mDateTimePicker = new DateTimePicker(context); + + // 2. 将控件填充到 Dialog 的内容区域 setView(mDateTimePicker); + + // 3. 设置联动监听: + // 当内部 DateTimePicker 的滚轮发生变化时,同步更新 mDate,并刷新 Dialog 的标题 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, - int dayOfMonth, int hourOfDay, int minute) { + int dayOfMonth, int hourOfDay, int minute) { 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()); } }); + + // 4. 初始化时间状态 mDate.setTimeInMillis(date); - mDate.set(Calendar.SECOND, 0); + mDate.set(Calendar.SECOND, 0); // 归零秒数 mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + + // 5. 设置底部按钮 setButton(context.getString(R.string.datetime_dialog_ok), this); setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + + // 6. 根据系统设置决定是否使用 24小时制 set24HourView(DateFormat.is24HourFormat(this.getContext())); updateTitle(mDate.getTimeInMillis()); } @@ -72,15 +85,23 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener mOnDateTimeSetListener = callBack; } + /** + * 更新对话框标题 + * 使用 DateUtils 格式化时间戳,例如显示为 "2025年1月1日 12:00" + */ 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; flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } + /** + * 处理“确定”按钮点击事件 + * 将最终选中的时间戳回调给调用者。 + */ public void onClick(DialogInterface arg0, int arg1) { if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); diff --git a/src/src/net/micode/notes/ui/DropdownMenu.java b/src/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..a384192 100644 --- a/src/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/src/net/micode/notes/ui/DropdownMenu.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -27,17 +16,39 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import net.micode.notes.R; +/** + * 下拉菜单包装器 (UI Component Wrapper) + *

+ * 职责: + * 1. 封装 {@link PopupMenu} 与 {@link Button} 的绑定逻辑。 + * 2. 简化 Activity 中的菜单创建代码。 + * 3. 实现“点击按钮 -> 弹出菜单”的自动化交互。 + */ public class DropdownMenu { - private Button mButton; - private PopupMenu mPopupMenu; - private Menu mMenu; + private Button mButton; // 触发菜单的按钮 + private PopupMenu mPopupMenu; // Android 原生的弹出菜单组件 + private Menu mMenu; // 菜单的数据模型 + /** + * 构造函数:初始化下拉菜单 + * + * @param context 上下文 + * @param button 用于触发菜单的 UI 按钮 + * @param menuId 菜单资源 ID (R.menu.xxx),用于填充菜单项 + */ public DropdownMenu(Context context, Button button, int menuId) { mButton = button; + // 设置按钮背景为下拉图标 (通常是一个向下的小箭头) mButton.setBackgroundResource(R.drawable.dropdown_icon); + + // 创建 PopupMenu 并将其锚定 (Anchor) 到按钮上 mPopupMenu = new PopupMenu(context, mButton); mMenu = mPopupMenu.getMenu(); + + // 解析 XML 菜单资源并填充到 PopupMenu 中 mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + + // 设置点击事件:点击按钮即显示菜单 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mPopupMenu.show(); @@ -45,17 +56,32 @@ public class DropdownMenu { }); } + /** + * 设置菜单项点击监听器 + * 将监听器委托给内部的 PopupMenu 处理 + */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { if (mPopupMenu != null) { mPopupMenu.setOnMenuItemClickListener(listener); } } + /** + * 查找具体的菜单项 + * 常用于动态修改菜单项的标题或可见性 + * + * @param id 菜单项 ID (R.id.xxx) + * @return MenuItem 对象 + */ public MenuItem findItem(int id) { return mMenu.findItem(id); } + /** + * 修改按钮显示的文本标题 + * (例如在切换文件夹后,按钮文字变为当前文件夹名称) + */ public void setTitle(CharSequence title) { mButton.setText(title); } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/FoldersListAdapter.java b/src/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..6b67cbc 100644 --- a/src/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/src/net/micode/notes/ui/FoldersListAdapter.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -28,11 +17,20 @@ import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; - +/** + * 文件夹列表适配器 (UI Adapter Layer) + *

+ * 职责: + * 1. 将数据库中的文件夹数据适配到 ListView 或 Spinner 中显示。 + * 2. 主要用于“移动便签”功能中,供用户选择目标文件夹。 + * 3. 处理系统“根目录”的特殊显示名称(将 ID 为 0 的文件夹显示为 "桌面" 或 "上级文件夹")。 + */ public class FoldersListAdapter extends CursorAdapter { + + // 数据库查询投影:只查询 ID 和 名称(Snippet) public static final String [] PROJECTION = { - NoteColumns.ID, - NoteColumns.SNIPPET + NoteColumns.ID, + NoteColumns.SNIPPET }; public static final int ID_COLUMN = 0; @@ -40,34 +38,53 @@ public class FoldersListAdapter extends CursorAdapter { public FoldersListAdapter(Context context, Cursor c) { super(context, c); - // TODO Auto-generated constructor stub } + /** + * 创建新的视图项 (Inflate) + * 当 ListView 没有可复用的 View 时调用此方法。 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new FolderListItem(context); } + /** + * 绑定数据到视图 (Bind) + * 当 ListView 复用现有 View 时调用此方法,负责填充数据。 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof FolderListItem) { + // 特殊逻辑处理: + // 如果文件夹 ID 是 ROOT_FOLDER (0),则显示为固定的本地化字符串(如"桌面"),而不是数据库里的原始名称。 String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); } } + /** + * 获取指定位置文件夹的显示名称 + * 复用了 bindView 中的根目录判断逻辑。 + */ public String getFolderName(Context context, int position) { Cursor cursor = (Cursor) getItem(position); return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); } + /** + * 内部类:文件夹列表项视图 + * 继承自 LinearLayout,封装了布局的初始化和控件查找,相当于 ViewHolder 的变体。 + */ private class FolderListItem extends LinearLayout { private TextView mName; public FolderListItem(Context context) { super(context); + // 加载布局文件 folder_list_item.xml inflate(context, R.layout.folder_list_item, this); mName = (TextView) findViewById(R.id.tv_folder_name); } @@ -77,4 +94,4 @@ public class FoldersListAdapter extends CursorAdapter { } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NoteEditActivity.java b/src/src/net/micode/notes/ui/NoteEditActivity.java index 96a9ff8..5a8bf80 100644 --- a/src/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/src/net/micode/notes/ui/NoteEditActivity.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -70,20 +59,36 @@ import java.util.HashSet; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; - - +import org.json.JSONArray; +import org.json.JSONObject; +import net.micode.notes.ai.AIService; +import android.app.ProgressDialog; + +/** + * 便签编辑页面 (UI Layer - Core Activity) + *

+ * 职责: + * 1. 提供便签内容的编辑界面,支持普通文本模式和清单模式 (CheckList)。 + * 2. 处理背景色、字体大小、闹钟提醒等设置。 + * 3. 负责 Activity 生命周期内的状态保存与恢复。 + * 4. 【2025新特性】集成 AI 智能助手 (润色与分类)。 + *

+ * 架构关系: + * 持有 {@link WorkingNote} (ViewModel) 实例,通过它操作底层数据。 + * 实现 {@link OnClickListener} 处理 UI 点击事件。 + */ public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { - private class HeadViewHolder { - public TextView tvModified; - - public ImageView ivAlertIcon; - public TextView tvAlertDate; - - public ImageView ibSetBgColor; + // 内部类:用于持有标题栏的 UI 控件引用 (ViewHolder模式) + private class HeadViewHolder { + public TextView tvModified; // 修改时间 + public ImageView ivAlertIcon; // 闹钟图标 + public TextView tvAlertDate; // 闹钟时间 + public ImageView ibSetBgColor; // 设置背景色按钮 } + // 静态映射表:将 UI 按钮 ID 映射到 逻辑颜色 ID private static final Map sBgSelectorBtnsMap = new HashMap(); static { sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); @@ -93,6 +98,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); } + // 静态映射表:将 逻辑颜色 ID 映射到 选中状态图标 ID private static final Map sBgSelectorSelectionMap = new HashMap(); static { sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); @@ -102,6 +108,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); } + // 静态映射表:字体大小按钮映射 private static final Map sFontSizeBtnsMap = new HashMap(); static { sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); @@ -110,6 +117,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); } + // AI 菜单项的 ID (虽然主要逻辑已迁移至 XML 按钮,但保留此常量兼容旧代码) + private static final int MENU_AI_OPT_ID = 999; + + // 静态映射表:字体选中状态映射 private static final Map sFontSelectorSelectionMap = new HashMap(); static { sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); @@ -120,32 +131,29 @@ public class NoteEditActivity extends Activity implements OnClickListener, private static final String TAG = "NoteEditActivity"; + // UI 控件引用 private HeadViewHolder mNoteHeaderHolder; - private View mHeadViewPanel; + private View mNoteBgColorSelector; // 背景色选择面板 + private View mFontSizeSelector; // 字体大小选择面板 + private EditText mNoteEditor; // 核心编辑框 (普通模式) + private View mNoteEditorPanel; // 编辑区域容器 + private LinearLayout mEditTextList; // 清单模式下的容器 (CheckList) - private View mNoteBgColorSelector; - - private View mFontSizeSelector; - - private EditText mNoteEditor; - - private View mNoteEditorPanel; - + // 业务逻辑核心对象 (ViewModel) private WorkingNote mWorkingNote; private SharedPreferences mSharedPrefs; private int mFontSizeId; private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; - private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + // 清单模式下的勾选/未勾选标记符号 public static final String TAG_CHECKED = String.valueOf('\u221A'); public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); - private LinearLayout mEditTextList; - + // 搜索相关 private String mUserQuery; private Pattern mPattern; @@ -154,16 +162,22 @@ public class NoteEditActivity extends Activity implements OnClickListener, super.onCreate(savedInstanceState); this.setContentView(R.layout.note_edit); + // 初始化 Activity 状态(加载数据或新建便签) if (savedInstanceState == null && !initActivityState(getIntent())) { finish(); return; } - initResources(); + initResources(); // 初始化 UI 控件 + + // === 【新增 AI 功能入口】 === + // 在 XML 布局中我们植入了一个 ID 为 btn_ai_polish 的 ImageButton + // 这里手动绑定点击监听器,触发 AI 逻辑 + findViewById(R.id.btn_ai_polish).setOnClickListener(this); + // ============================ } /** - * Current activity may be killed when the memory is low. Once it is killed, for another time - * user load this activity, we should restore the former state + * 处理 Activity 意外销毁后的状态恢复 (如内存不足时) */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { @@ -179,24 +193,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 初始化 Activity 状态的核心逻辑 + * 根据 Intent 的 Action 判断是 "查看/编辑旧便签" 还是 "新建便签" + */ private boolean initActivityState(Intent intent) { - /** - * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, - * then jump to the NotesListActivity - */ mWorkingNote = null; + + // Case 1: 查看或编辑已有便签 if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); mUserQuery = ""; - /** - * Starting from the searched result - */ + // 处理来自搜索结果的跳转,可能会带有高亮关键词 (User Query) if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); } + // 校验便签是否存在 if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { Intent jump = new Intent(this, NotesListActivity.class); startActivity(jump); @@ -204,6 +219,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, finish(); return false; } else { + // 加载数据 mWorkingNote = WorkingNote.load(this, noteId); if (mWorkingNote == null) { Log.e(TAG, "load note failed with note id" + noteId); @@ -211,11 +227,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } } + // 此时不自动弹出软键盘 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - } else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { - // New note + } + // Case 2: 新建便签 (INSERT_OR_EDIT) + else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); @@ -224,7 +242,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, ResourceParser.getDefaultBgId(this)); - // Parse call-record note + // 处理从通话记录创建便签的特殊逻辑 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); if (callDate != 0 && phoneNumber != null) { @@ -232,6 +250,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.w(TAG, "The call record number is null"); } long noteId = 0; + // 如果该通话记录已经有对应的便签,则打开它 if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), phoneNumber, callDate)) > 0) { mWorkingNote = WorkingNote.load(this, noteId); @@ -241,15 +260,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } } else { + // 否则创建新的通话便签 mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); mWorkingNote.convertToCallNote(phoneNumber, callDate); } } else { + // 创建普通空便签 mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); } + // 新建时自动弹出软键盘 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); @@ -258,6 +280,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, finish(); return false; } + // 注册监听器,当 WorkingNote 属性改变时回调本 Activity 刷新 UI mWorkingNote.setOnSettingStatusChangedListener(this); return true; } @@ -265,36 +288,47 @@ public class NoteEditActivity extends Activity implements OnClickListener, @Override protected void onResume() { super.onResume(); - initNoteScreen(); + initNoteScreen(); // 每次界面可见时刷新 UI 状态 } + /** + * 刷新便签编辑界面 + * 设置字体、背景、内容、修改时间等 + */ private void initNoteScreen() { mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); + + // 根据模式决定显示 EditText 还是 ListLayout if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { switchToListMode(mWorkingNote.getContent()); } else { + // 普通模式:设置文本并处理搜索高亮 mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); mNoteEditor.setSelection(mNoteEditor.getText().length()); } + + // 重置背景色选择器的选中状态 for (Integer id : sBgSelectorSelectionMap.keySet()) { findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); } + + // 应用背景资源 mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 显示最后修改时间 mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR)); - /** - * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker - * is not ready - */ - showAlertHeader(); + showAlertHeader(); // 刷新闹钟状态 } + /** + * 显示或隐藏闹钟提示头 + */ private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); @@ -321,11 +355,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - /** - * For new note without note id, we should firstly save it to - * generate a id. If the editing note is not worth saving, there - * is no id which is equivalent to create new note - */ + // 如果是尚未保存的新便签,系统回收前先保存一次以生成 ID if (!mWorkingNote.existInDatabase()) { saveNote(); } @@ -333,6 +363,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } + /** + * 处理点击事件分发 + * 点击面板外部区域时自动关闭颜色选择器或字体选择器 + */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE @@ -358,8 +392,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, || ev.getX() > (x + view.getWidth()) || ev.getY() < y || ev.getY() > (y + view.getHeight())) { - return false; - } + return false; + } return true; } @@ -386,26 +420,27 @@ public class NoteEditActivity extends Activity implements OnClickListener, }; mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); - /** - * HACKME: Fix bug of store the resource id in shared preference. - * The id may larger than the length of resources, in this case, - * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} - */ + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; } mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); } + /** + * 当 Activity 暂停时(如切到后台或锁屏),自动保存数据 + */ @Override protected void onPause() { super.onPause(); + // saveNote() 方法已经过改造,内部使用线程池异步保存,不会阻塞主线程 if(saveNote()) { Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); } clearSettingState(); } + // 更新桌面 Widget 内容 private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { @@ -418,29 +453,61 @@ public class NoteEditActivity extends Activity implements OnClickListener, } intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - mWorkingNote.getWidgetId() + mWorkingNote.getWidgetId() }); sendBroadcast(intent); setResult(RESULT_OK, intent); } + /** + * 全局点击事件处理 + */ public void onClick(View v) { int id = v.getId(); + + // ============================================= + // [新增] AI 智能助手入口 + // 点击放大镜图标,弹出功能选择对话框 + // ============================================= + if (id == R.id.btn_ai_polish) { + new AlertDialog.Builder(this) + .setTitle("AI 助手") + .setItems(new String[]{"✨ 文本润色", "🏷️ 智能分类 & 打标签"}, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + startDeepSeekOptimization(); // 功能1:润色 + } else { + performAIClassification(); // 功能2:分类 + } + } + }) + .show(); + return; + } + + // 处理更改背景色按钮 if (id == R.id.btn_set_bg_color) { mNoteBgColorSelector.setVisibility(View.VISIBLE); findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - - View.VISIBLE); - } else if (sBgSelectorBtnsMap.containsKey(id)) { + View.VISIBLE); + } + // 处理具体背景色点击 + else if (sBgSelectorBtnsMap.containsKey(id)) { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.GONE); mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); mNoteBgColorSelector.setVisibility(View.GONE); - } else if (sFontSizeBtnsMap.containsKey(id)) { + } + // 处理具体字体大小点击 + else if (sFontSizeBtnsMap.containsKey(id)) { findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); mFontSizeId = sFontSizeBtnsMap.get(id); mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + + // 字体改变后需要刷新视图 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { getWorkingText(); switchToListMode(mWorkingNote.getContent()); @@ -457,11 +524,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, if(clearSettingState()) { return; } - saveNote(); super.onBackPressed(); } + // 隐藏设置面板 private boolean clearSettingState() { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { mNoteBgColorSelector.setVisibility(View.GONE); @@ -473,6 +540,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } + // WorkingNote 回调:背景色变更时刷新界面 public void onBackgroundColorChanged() { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.VISIBLE); @@ -487,11 +555,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, } clearSettingState(); menu.clear(); + // 根据文件夹类型加载不同菜单 if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { getMenuInflater().inflate(R.menu.call_note_edit, menu); } else { getMenuInflater().inflate(R.menu.note_edit, menu); } + + // 兼容性代码:虽然已通过 XML 添加按钮,但保留此代码以支持某些旧设备的 Menu 键 + menu.add(0, MENU_AI_OPT_ID, 1, "AI 润色") + .setIcon(android.R.drawable.ic_menu_search) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); } else { @@ -508,6 +583,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + case MENU_AI_OPT_ID: // 菜单栏的 AI 选项 + startDeepSeekOptimization(); + return true; case R.id.menu_new_note: createNewNote(); break; @@ -553,6 +631,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + // 弹出时间选择器设置提醒 private void setReminder() { DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); d.setOnDateTimeSetListener(new OnDateTimeSetListener() { @@ -563,10 +642,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, d.show(); } - /** - * Share note to apps that support {@link Intent#ACTION_SEND} action - * and {@text/plain} type - */ + // 调用系统分享 private void sendTo(Context context, String info) { Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, info); @@ -574,11 +650,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, context.startActivity(intent); } + // 保存当前并创建新便签 private void createNewNote() { - // Firstly, save current editing notes saveNote(); - - // For safety, start a new NoteEditActivity finish(); Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); @@ -586,6 +660,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, startActivity(intent); } + // 删除便签 private void deleteCurrentNote() { if (mWorkingNote.existInDatabase()) { HashSet ids = new HashSet(); @@ -613,10 +688,6 @@ public class NoteEditActivity extends Activity implements OnClickListener, } public void onClockAlertChanged(long date, boolean set) { - /** - * User could set clock to an unsaved note, so before setting the - * alert clock, we should save the note first - */ if (!mWorkingNote.existInDatabase()) { saveNote(); } @@ -632,11 +703,6 @@ public class NoteEditActivity extends Activity implements OnClickListener, alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); } } else { - /** - * There is the condition that user has input nothing (the note is - * not worthy saving), we have no note id, remind the user that he - * should input something - */ Log.e(TAG, "Clock alert setting error"); showToast(R.string.error_note_empty_for_clock); } @@ -646,6 +712,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, updateWidget(); } + // 处理清单项删除 public void onEditTextDelete(int index, String text) { int childCount = mEditTextList.getChildCount(); if (childCount == 1) { @@ -672,10 +739,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, edit.setSelection(length); } + // 处理清单项回车(新增一行) public void onEditTextEnter(int index, String text) { - /** - * Should not happen, check for debug - */ if(index > mEditTextList.getChildCount()) { Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); } @@ -691,6 +756,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + // 切换到清单模式:将文本拆分为列表项 private void switchToListMode(String text) { mEditTextList.removeAllViews(); String[] items = text.split("\n"); @@ -708,6 +774,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList.setVisibility(View.VISIBLE); } + // 获取带有高亮的 Spannable 文本 (用于搜索结果高亮) private Spannable getHighlightQueryResult(String fullText, String userQuery) { SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); if (!TextUtils.isEmpty(userQuery)) { @@ -725,6 +792,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, return spannable; } + // 生成清单列表项 View private View getListItem(String item, int index) { View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); @@ -805,28 +873,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, return hasChecked; } + // 触发 WorkingNote 保存数据 private boolean saveNote() { getWorkingText(); boolean saved = mWorkingNote.saveNote(); if (saved) { - /** - * There are two modes from List view to edit view, open one note, - * create/edit a node. Opening node requires to the original - * position in the list when back from edit view, while creating a - * new node requires to the top of the list. This code - * {@link #RESULT_OK} is used to identify the create/edit state - */ setResult(RESULT_OK); } return saved; } private void sendToDesktop() { - /** - * Before send message to home, we should make sure that current - * editing note is exists in databases. So, for new note, firstly - * save it - */ if (!mWorkingNote.existInDatabase()) { saveNote(); } @@ -846,11 +903,6 @@ public class NoteEditActivity extends Activity implements OnClickListener, showToast(R.string.info_note_enter_desktop); sendBroadcast(sender); } else { - /** - * There is the condition that user has input nothing (the note is - * not worthy saving), we have no note id, remind the user that he - * should input something - */ Log.e(TAG, "Send to desktop error"); showToast(R.string.error_note_empty_for_send_to_desktop); } @@ -870,4 +922,119 @@ public class NoteEditActivity extends Activity implements OnClickListener, private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } -} + + // ================================================================================= + // [新功能] 启动 DeepSeek 文本润色流程 + // 逻辑:获取文本 -> 显示Loading -> 调用AIService -> 成功后弹窗询问是否替换 + // ================================================================================= + private void startDeepSeekOptimization() { + // 1. 获取当前编辑框里的文字 + final String currentText = mNoteEditor.getText().toString(); + + if (currentText.trim().length() == 0) { + Toast.makeText(this, "请先写点内容再让 AI 润色哦~", Toast.LENGTH_SHORT).show(); + return; + } + + // 2. 弹出一个加载框 (Loading) + final ProgressDialog progressDialog = new ProgressDialog(this); + progressDialog.setMessage("DeepSeek 正在思考中..."); + progressDialog.setCancelable(false); + progressDialog.show(); + + // 3. 呼叫后台的 AI 服务 + AIService.callDeepSeek(currentText, new AIService.AIResultCallback() { + @Override + public void onSuccess(final String result) { + progressDialog.dismiss(); + + // 4. 弹出结果对话框 + new AlertDialog.Builder(NoteEditActivity.this) + .setTitle("✨ DeepSeek 优化结果") + .setMessage(result) + .setPositiveButton("替换原文", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mNoteEditor.setText(result); + mNoteEditor.setSelection(result.length()); + mWorkingNote.setWorkingText(result); // 触发保存 + } + }) + .setNegativeButton("添加到末尾", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String newText = currentText + "\n\n--- AI 建议 ---\n" + result; + mNoteEditor.setText(newText); + mNoteEditor.setSelection(newText.length()); + mWorkingNote.setWorkingText(newText); + } + }) + .setNeutralButton("取消", null) + .show(); + } + + @Override + public void onError(final String error) { + progressDialog.dismiss(); + Toast.makeText(NoteEditActivity.this, "AI 罢工了: " + error, Toast.LENGTH_LONG).show(); + } + }); + } + + // ================================================================================= + // [新功能] 启动 AI 智能分类流程 + // 逻辑:获取文本 -> 调用AIService分类接口 -> 解析JSON -> 自动设置背景色和插入标签 + // ================================================================================= + private void performAIClassification() { + final String content = mNoteEditor.getText().toString(); + if (content.trim().length() == 0) { + Toast.makeText(this, "内容为空,无法分类", Toast.LENGTH_SHORT).show(); + return; + } + + final ProgressDialog pd = ProgressDialog.show(this, "DeepSeek", "正在分析内容并分类...", true, false); + + AIService.classifyNote(content, new AIService.AIResultCallback() { + @Override + public void onSuccess(String result) { + pd.dismiss(); + try { + // 1. 解析 DeepSeek 返回的 JSON 格式数据 + JSONObject json = new JSONObject(result); + int colorId = json.optInt("color_id", 0); // 获取建议颜色 + JSONArray tags = json.optJSONArray("tags"); // 获取建议标签 + + // 2. 自动改变便签背景颜色 + mWorkingNote.setBgColorId(colorId); + mNoteBgColorSelector.setVisibility(View.GONE); + + // 3. 将标签插入到正文头部 + String tagString = ""; + if (tags != null) { + for (int i = 0; i < tags.length(); i++) { + tagString += "【" + tags.getString(i) + "】"; + } + } + + String newText = tagString + "\n" + content; + mNoteEditor.setText(newText); + mWorkingNote.setWorkingText(newText); // 触发保存 + + // 4. Toast 提示用户分类结果 + String[] categories = {"生活", "紧急", "工作", "旅行", "灵感"}; + String catName = (colorId >= 0 && colorId < categories.length) ? categories[colorId] : "未知"; + Toast.makeText(NoteEditActivity.this, "已分类为:" + catName, Toast.LENGTH_LONG).show(); + + } catch (Exception e) { + Toast.makeText(NoteEditActivity.this, "AI 返回格式错误: " + result, Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onError(String error) { + pd.dismiss(); + Toast.makeText(NoteEditActivity.this, error, Toast.LENGTH_SHORT).show(); + } + }); + } +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NoteEditText.java b/src/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..8068537 100644 --- a/src/src/net/micode/notes/ui/NoteEditText.java +++ b/src/src/net/micode/notes/ui/NoteEditText.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -37,15 +26,31 @@ import net.micode.notes.R; import java.util.HashMap; import java.util.Map; +/** + * 自定义便签编辑框 (UI Component) + *

+ * 职责: + * 1. 增强原生 EditText,专门用于处理“清单模式”下的多行交互。 + * 2. 拦截软键盘按键事件: + * - Enter 键:分割当前行,在下方插入新项目。 + * - Delete 键:如果光标在行首,则删除当前行并将其内容合并到上一行。 + * 3. 处理文本中的超链接 (电话、网址、邮件) 并提供上下文菜单跳转。 + * 4. 优化触摸点击时的光标定位逻辑。 + */ 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:" ; + // 链接类型与资源 ID 的映射表 private static final Map sSchemaActionResMap = new HashMap(); static { sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); @@ -54,23 +59,25 @@ public class NoteEditText extends EditText { } /** - * 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 + * 当用户在行首按 Delete 键时触发 (合并行) + * @param index 当前行索引 + * @param text 当前行剩余的文本 (将被追加到上一行) */ void onEditTextDelete(int index, String text); /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen + * 当用户按 Enter 键时触发 (分裂行/新增行) + * @param index 新行的索引 + * @param text 移到新行的文本内容 */ void onEditTextEnter(int index, String text); /** - * Hide or show item option when text change + * 当文本内容或焦点发生变化时触发 (用于控制复选框的显示/隐藏) */ void onTextChange(int index, boolean hasText); } @@ -96,14 +103,19 @@ public class NoteEditText extends EditText { public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub } + /** + * 处理触摸事件 + *

+ * 逻辑说明: + * 这里手动计算了点击坐标对应的字符偏移量,并强制设置光标位置。 + * 这样做是为了解决在某些复杂布局或包含富文本时,原生 EditText 光标定位不准的问题。 + */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - int x = (int) event.getX(); int y = (int) event.getY(); x -= getTotalPaddingLeft(); @@ -121,15 +133,21 @@ public class NoteEditText extends EditText { return super.onTouchEvent(event); } + /** + * 键盘按下事件拦截 + * 记录关键状态,实际逻辑在 onKeyUp 中执行。 + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: + // 拦截 Enter 键,防止原生换行(因为我们要创建新 Item) if (mOnTextViewChangeListener != null) { return false; } break; case KeyEvent.KEYCODE_DEL: + // 记录按下删除键瞬间的光标位置,用于 onKeyUp 判断是否在行首 mSelectionStartBeforeDelete = getSelectionStart(); break; default: @@ -138,10 +156,14 @@ public class NoteEditText extends EditText { return super.onKeyDown(keyCode, event); } + /** + * 键盘抬起事件处理 (核心交互逻辑) + */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_DEL: + // 逻辑:如果删除前光标在 0 位置,且不是第一行,说明用户想合并到上一行 if (mOnTextViewChangeListener != null) { if (0 == mSelectionStartBeforeDelete && mIndex != 0) { mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); @@ -152,10 +174,11 @@ public class NoteEditText extends EditText { } break; case KeyEvent.KEYCODE_ENTER: + // 逻辑:将当前光标后的文字剪切下来,传给 Listener 创建新行 if (mOnTextViewChangeListener != null) { int selectionStart = getSelectionStart(); String text = getText().subSequence(selectionStart, length()).toString(); - setText(getText().subSequence(0, selectionStart)); + setText(getText().subSequence(0, selectionStart)); // 保留光标前的内容 mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { Log.d(TAG, "OnTextViewChangeListener was not seted"); @@ -167,6 +190,10 @@ public class NoteEditText extends EditText { return super.onKeyUp(keyCode, event); } + /** + * 焦点变化处理 + * 当失去焦点且内容为空时,通知父容器可能需要隐藏该项前面的复选框或占位符。 + */ @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener != null) { @@ -179,6 +206,13 @@ public class NoteEditText extends EditText { super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * 创建上下文菜单 (长按菜单) + *

+ * 逻辑: + * 检查选中的文本是否包含 URLSpan (电话、网址等)。 + * 如果包含,添加对应的菜单项(如“拨打电话”、“打开网页”),并处理点击跳转。 + */ @Override protected void onCreateContextMenu(ContextMenu menu) { if (getText() instanceof Spanned) { @@ -205,7 +239,7 @@ public class NoteEditText extends EditText { menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // goto a new intent + // 执行跳转 Intent urls[0].onClick(NoteEditText.this); return true; } @@ -214,4 +248,4 @@ public class NoteEditText extends EditText { } super.onCreateContextMenu(menu); } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NoteItemData.java b/src/src/net/micode/notes/ui/NoteItemData.java index 0f5a878..68087a8 100644 --- a/src/src/net/micode/notes/ui/NoteItemData.java +++ b/src/src/net/micode/notes/ui/NoteItemData.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -25,23 +14,34 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.tool.DataUtils; - +/** + * 便签列表项数据模型 (UI Data Model) + *

+ * 职责: + * 1. 封装便签列表页所需的单条数据。 + * 2. 将数据库 Cursor 中的原始字段解析为 Java 字段。 + * 3. 处理特殊业务逻辑(如通话记录联系人查找、摘要清洗)。 + * 4. 计算列表项的位置状态(首项、末项、跟随文件夹等),用于 UI 背景渲染。 + */ public class NoteItemData { + + // 列表页查询所需的列投影 (Projection) 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, + 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, }; + // 列索引常量,对应 PROJECTION 数组 private static final int ID_COLUMN = 0; private static final int ALERTED_DATE_COLUMN = 1; private static final int BG_COLOR_ID_COLUMN = 2; @@ -55,6 +55,7 @@ public class NoteItemData { 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; @@ -63,19 +64,23 @@ public class NoteItemData { private long mModifiedDate; private int mNotesCount; private long mParentId; - private String mSnippet; - private int mType; + private String mSnippet; // 摘要 + private int mType; // 类型 (便签/文件夹) private int mWidgetId; private int mWidgetType; - private String mName; - private String mPhoneNumber; + private String mName; // 联系人名字 (仅通话记录便签有效) + private String mPhoneNumber; // 电话号码 (仅通话记录便签有效) + // UI 位置状态标记 (用于决定背景图的圆角样式) private boolean mIsLastItem; private boolean mIsFirstItem; private boolean mIsOnlyOneItem; - private boolean mIsOneNoteFollowingFolder; - private boolean mIsMultiNotesFollowingFolder; + private boolean mIsOneNoteFollowingFolder; // 是否是紧跟在文件夹后的单条便签 + private boolean mIsMultiNotesFollowingFolder; // 是否是紧跟在文件夹后的多条便签组 + /** + * 构造函数:从 Cursor 解析数据 + */ public NoteItemData(Context context, Cursor cursor) { mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); @@ -86,13 +91,17 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + + // 清洗摘要:如果是清单模式,去掉前面的 "√" 或 "□" 符号,只显示文字 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); mPhoneNumber = ""; + // 特殊逻辑:如果是通话记录文件夹下的便签,需要查找联系人名称 if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); if (!TextUtils.isEmpty(mPhoneNumber)) { @@ -106,9 +115,19 @@ public class NoteItemData { if (mName == null) { mName = ""; } + + // 计算当前项在列表中的相对位置状态 checkPostion(cursor); } + /** + * 检查并设置列表项的位置状态。 + *

+ * 逻辑说明: + * 为了实现类似 iOS 分组列表的 UI 效果(第一项顶部圆角,中间项无圆角,最后一项底部圆角), + * 我们需要知道当前项相对于其他项的位置关系。 + * 特别是,需要探测上一项是否为“文件夹”,以决定便签分组的起始样式。 + */ private void checkPostion(Cursor cursor) { mIsLastItem = cursor.isLast() ? true : false; mIsFirstItem = cursor.isFirst() ? true : false; @@ -116,17 +135,24 @@ public class NoteItemData { mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; + // 如果当前是便签,且不是列表的第一项 if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { int position = cursor.getPosition(); + + // 关键逻辑:临时回退游标到上一项进行探测 if (cursor.moveToPrevious()) { + // 如果上一项是文件夹或系统文件夹 if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 如果总数大于 (当前位置+1),说明后面还有便签 -> MultiNotesFollowingFolder if (cursor.getCount() > (position + 1)) { mIsMultiNotesFollowingFolder = true; } else { + // 否则说明这是最后一项 -> OneNoteFollowingFolder mIsOneNoteFollowingFolder = true; } } + // 探测完毕,必须将游标移回当前位置,否则会导致遍历错乱 if (!cursor.moveToNext()) { throw new IllegalStateException("cursor move to previous but can't move back"); } @@ -221,4 +247,4 @@ public class NoteItemData { public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NotesListActivity.java b/src/src/net/micode/notes/ui/NotesListActivity.java index e843aec..ffcdc0d 100644 --- a/src/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/src/net/micode/notes/ui/NotesListActivity.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -78,55 +67,63 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; +/** + * 便签主列表页面 (UI Layer - Main Entry) + *

+ * 职责: + * 1. 展示便签和文件夹的混合列表。 + * 2. 处理文件夹导航 (进入子文件夹/返回根目录)。 + * 3. 管理批量操作模式 (长按进入 ActionMode,支持批量删除、移动)。 + * 4. 负责数据的异步查询与刷新 (AsyncQueryHandler)。 + * 5. 【2025新特性】集成 AI 智能排序 (按颜色/语义聚类)。 + */ public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { - private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; - private static final int FOLDER_LIST_QUERY_TOKEN = 1; + // 异步查询的 Token,用于区分不同的查询请求 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 查询便签列表 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 查询文件夹列表 (用于移动便签时) + // 文件夹上下文菜单 ID private static final int MENU_FOLDER_DELETE = 0; - private static final int MENU_FOLDER_VIEW = 1; - private static final int MENU_FOLDER_CHANGE_NAME = 2; private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + // 列表当前的编辑状态枚举 private enum ListEditState { - NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + NOTE_LIST, // 正常根目录列表 + SUB_FOLDER, // 子文件夹内部 + CALL_RECORD_FOLDER // 通话记录专用文件夹 }; private ListEditState mState; + // 异步查询处理器,负责在后台线程执行 ContentProvider 查询 private BackgroundQueryHandler mBackgroundQueryHandler; private NotesListAdapter mNotesListAdapter; - private ListView mNotesListView; - private Button mAddNewNote; private boolean mDispatch; - private int mOriginY; - private int mDispatchY; private TextView mTitleBar; - - private long mCurrentFolderId; - + private long mCurrentFolderId; // 当前所在的文件夹 ID private ContentResolver mContentResolver; - - private ModeCallback mModeCallBack; + private ModeCallback mModeCallBack; // 批量模式回调 private static final String TAG = "NotesListActivity"; - public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; - private NoteItemData mFocusNoteDataItem; + private NoteItemData mFocusNoteDataItem; // 当前长按选中的便签项 + // SQL 查询条件:普通模式 (查询指定文件夹下的便签) private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // SQL 查询条件:根目录模式 (排除系统文件夹,包含通话记录文件夹) private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " @@ -142,7 +139,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt initResources(); /** - * Insert an introduction when user firstly use this application + * 首次启动应用时,自动创建一条介绍便签。 + * 这对于引导新用户非常有帮助。 */ setAppInfoFromRawRes(); } @@ -151,19 +149,21 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + // 如果从编辑页返回且数据有变动,刷新列表 mNotesListAdapter.changeCursor(null); } else { super.onActivityResult(requestCode, resultCode, data); } } + // 从 raw 资源读取介绍文本并创建便签 private void setAppInfoFromRawRes() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { StringBuilder sb = new StringBuilder(); InputStream in = null; try { - in = getResources().openRawResource(R.raw.introduction); + in = getResources().openRawResource(R.raw.introduction); if (in != null) { InputStreamReader isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr); @@ -184,7 +184,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt try { in.close(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } } @@ -206,6 +205,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt @Override protected void onStart() { super.onStart(); + // 每次页面可见时,重新发起查询以刷新数据 startAsyncNotesListQuery(); } @@ -231,6 +231,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mModeCallBack = new ModeCallback(); } + /** + * 批量操作模式回调 (MultiChoiceModeListener) + * 处理长按进入的ActionMode,包括全选、删除、移动等操作。 + */ private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; @@ -252,6 +256,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mNotesListView.setLongClickable(false); mAddNewNote.setVisibility(View.GONE); + // 自定义 ActionMode 的标题栏,植入全选下拉菜单 View customView = LayoutInflater.from(NotesListActivity.this).inflate( R.layout.note_list_dropdown_menu, null); mode.setCustomView(customView); @@ -271,7 +276,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private void updateMenu() { int selectedCount = mNotesListAdapter.getSelectedCount(); - // Update dropdown menu String format = getResources().getString(R.string.menu_select_title, selectedCount); mDropDownMenu.setTitle(format); MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); @@ -287,12 +291,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - // TODO Auto-generated method stub return false; } public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - // TODO Auto-generated method stub return false; } @@ -307,7 +309,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { + boolean checked) { mNotesListAdapter.setCheckedItem(position, checked); updateMenu(); } @@ -325,14 +327,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.setTitle(getString(R.string.alert_title_delete)); builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(getString(R.string.alert_message_delete_notes, - mNotesListAdapter.getSelectedCount())); + mNotesListAdapter.getSelectedCount())); builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - batchDelete(); - } - }); + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); break; @@ -346,6 +348,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 新建便签按钮的触摸监听器 + *

+ * 逻辑说明 (HACKME): + * "New Note" 按钮有一部分背景是透明的。为了让用户点到透明区域时能穿透点击到下方的列表项, + * 这里通过计算坐标,手动将事件 Dispatch 给 ListView。 + * 公式 y = -0.12x + 94 是根据 UI 设计图的形状拟合出来的。 + */ private class NewNoteOnTouchListener implements OnTouchListener { public boolean onTouch(View v, MotionEvent event) { @@ -356,22 +366,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt int newNoteViewHeight = mAddNewNote.getHeight(); int start = screenHeight - newNoteViewHeight; int eventY = start + (int) event.getY(); - /** - * Minus TitleBar's height - */ if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); } - /** - * HACKME:When click the transparent part of "New Note" button, dispatch - * the event to the list view behind this button. The transparent part of - * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) - * and the line top of the button. The coordinate based on left of the "New - * Note" button. The 94 represents maximum height of the transparent part. - * Notice that, if the background of the button changes, the formula should - * also change. This is very bad, just for the UI designer's strong requirement. - */ if (event.getY() < (event.getX() * (-0.12) + 94)) { View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); @@ -408,13 +406,23 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }; + /** + * 启动异步便签列表查询 (核心逻辑) + *

+ * 【AI 智能分类关键修改】: + * 修改了 SQL 的 ORDER BY 子句。 + * 原逻辑:按类型 -> 按时间。 + * 新逻辑:按类型 -> 按颜色 ID (BG_COLOR_ID) -> 按时间。 + * 这样相同颜色的便签(代表相同语义分类)会在列表中自动聚类显示。 + */ private void startAsyncNotesListQuery() { String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { - String.valueOf(mCurrentFolderId) - }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } private final class BackgroundQueryHandler extends AsyncQueryHandler { @@ -469,20 +477,20 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } + // 执行批量删除 private void batchDelete() { new AsyncTask>() { protected HashSet doInBackground(Void... unused) { HashSet widgets = mNotesListAdapter.getSelectedWidget(); if (!isSyncMode()) { - // if not synced, delete notes directly + // 如果未开启云同步,直接删除数据 if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter .getSelectedItemIds())) { } else { Log.e(TAG, "Delete notes error, should not happens"); } } else { - // in sync mode, we'll move the deleted note into the trash - // folder + // 如果开启了云同步,将数据移动到回收站文件夹,以便同步到服务器 if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { Log.e(TAG, "Move notes to trash folder error, should not happens"); @@ -517,10 +525,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); if (!isSyncMode()) { - // if not synced, delete folder directly DataUtils.batchDeleteNotes(mContentResolver, ids); } else { - // in sync mode, we'll move the deleted folder into the trash folder DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); } if (widgets != null) { @@ -540,9 +546,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } + // 进入子文件夹 private void openFolder(NoteItemData data) { mCurrentFolderId = data.getId(); - startAsyncNotesListQuery(); + startAsyncNotesListQuery(); // 刷新列表,查询子文件夹内容 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mState = ListEditState.CALL_RECORD_FOLDER; mAddNewNote.setVisibility(View.GONE); @@ -624,7 +631,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt values.put(NoteColumns.LOCAL_MODIFIED, 1); mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { - String.valueOf(mFocusNoteDataItem.getId()) + String.valueOf(mFocusNoteDataItem.getId()) }); } } else if (!TextUtils.isEmpty(name)) { @@ -640,13 +647,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } - /** - * When the name edit text is null, disable the positive button - */ + etName.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // TODO Auto-generated method stub - } public void onTextChanged(CharSequence s, int start, int before, int count) { @@ -658,8 +661,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public void afterTextChanged(Editable s) { - // TODO Auto-generated method stub - } }); } @@ -700,7 +701,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - appWidgetId + appWidgetId }); sendBroadcast(intent); @@ -920,7 +921,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private void startQueryDestinationFolders() { String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; selection = (mState == ListEditState.NOTE_LIST) ? selection: - "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, @@ -951,4 +952,4 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } return false; } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NotesListAdapter.java b/src/src/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..77c854c 100644 --- a/src/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/src/net/micode/notes/ui/NotesListAdapter.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -30,14 +19,33 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; - +/** + * 便签列表适配器 (UI Adapter Layer) + *

+ * 职责: + * 1. 将数据库 Cursor 数据适配到 ListView 中显示。 + * 2. 管理列表项的视图创建 (newView) 和数据绑定 (bindView)。 + * 3. 实现自定义的“批量多选模式” (Choice Mode),维护选中项的状态。 + * 4. 辅助计算选中的便签 ID 及关联的 Widget 信息,供批量操作使用。 + */ public class NotesListAdapter extends CursorAdapter { private static final String TAG = "NotesListAdapter"; private Context mContext; + + // 记录选中项的 Map: Key=列表位置(Position), Value=是否选中 + // 使用 HashMap 而不是 SparseBooleanArray 可能是为了更方便地获取 values 集合 private HashMap mSelectedIndex; + + // 当前列表中“便签”类型的总数 (不包含文件夹) private int mNotesCount; + + // 当前是否处于批量选择模式 private boolean mChoiceMode; + /** + * 简单的内部类,用于存储关联 Widget 的信息 + * 当删除便签时,需要根据这些信息去更新桌面小部件 + */ public static class AppWidgetAttribute { public int widgetId; public int widgetType; @@ -50,38 +58,71 @@ public class NotesListAdapter extends CursorAdapter { mNotesCount = 0; } + /** + * 创建新的列表项视图 + * 当 ListView 需要展示新项且没有可复用的 View 时调用。 + * 这里返回的是自定义视图 {@link NotesListItem}。 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new NotesListItem(context); } + /** + * 绑定数据到视图 + * 当 ListView 复用现有 View 时调用。 + * + * @param view 由 newView 创建的 NotesListItem + * @param context 上下文 + * @param cursor 指向当前数据行的游标 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof NotesListItem) { + // 将 Cursor 数据转换为强类型对象 NoteItemData NoteItemData itemData = new NoteItemData(context, cursor); + + // 调用自定义 View 的 bind 方法进行渲染 + // 传入 mChoiceMode 决定是否显示复选框 + // 传入 isSelectedItem 决定复选框是否被勾选 ((NotesListItem) view).bind(context, itemData, mChoiceMode, isSelectedItem(cursor.getPosition())); } } + /** + * 设置指定位置的选中状态 + */ public void setCheckedItem(final int position, final boolean checked) { mSelectedIndex.put(position, checked); - notifyDataSetChanged(); + notifyDataSetChanged(); // 刷新 UI 以显示勾选框变化 } public boolean isInChoiceMode() { return mChoiceMode; } + /** + * 切换普通模式/多选模式 + * 每次切换模式时,都会清空当前的选中状态 + */ public void setChoiceMode(boolean mode) { mSelectedIndex.clear(); mChoiceMode = mode; } + /** + * 全选/全不选逻辑 + *

+ * 关键逻辑: + * 遍历 Cursor,只选中类型为 {@link Notes#TYPE_NOTE} 的项。 + * 系统文件夹或普通文件夹不会被“全选”操作选中。 + */ public void selectAll(boolean checked) { Cursor cursor = getCursor(); for (int i = 0; i < getCount(); i++) { if (cursor.moveToPosition(i)) { + // 仅选中“便签”类型,忽略“文件夹” if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { setCheckedItem(i, checked); } @@ -89,10 +130,15 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 获取所有被选中的便签的数据库 ID 集合 + * 用于批量删除或移动操作。 + */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { if (mSelectedIndex.get(position) == true) { + // getItemId 是 CursorAdapter 的方法,返回当前位置 Cursor 的 _id 列 Long id = getItemId(position); if (id == Notes.ID_ROOT_FOLDER) { Log.d(TAG, "Wrong item id, should not happen"); @@ -105,6 +151,13 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取所有被选中便签关联的 Widget 信息 + *

+ * 场景: + * 用户在列表中删除了一个便签,如果这个便签在桌面上有对应的小部件, + * 我们需要获取这些 Widget 的 ID,以便发送广播通知桌面移除或更新它们。 + */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -116,9 +169,7 @@ public class NotesListAdapter extends CursorAdapter { widget.widgetId = item.getWidgetId(); widget.widgetType = item.getWidgetType(); itemSet.add(widget); - /** - * Don't close cursor here, only the adapter could close it - */ + // 注意:这里不要关闭 Cursor,因为它是由 Adapter 管理的 } else { Log.e(TAG, "Invalid cursor"); return null; @@ -128,6 +179,9 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取当前选中的项目数量 + */ public int getSelectedCount() { Collection values = mSelectedIndex.values(); if (null == values) { @@ -143,11 +197,18 @@ public class NotesListAdapter extends CursorAdapter { return count; } + /** + * 判断是否已全选 + * 对比选中数量和计算出的 mNotesCount (便签总数) + */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); return (checkedCount != 0 && checkedCount == mNotesCount); } + /** + * 检查指定位置是否被选中 + */ public boolean isSelectedItem(final int position) { if (null == mSelectedIndex.get(position)) { return false; @@ -155,18 +216,28 @@ public class NotesListAdapter extends CursorAdapter { return mSelectedIndex.get(position); } + // 当底层数据改变时触发 @Override protected void onContentChanged() { super.onContentChanged(); calcNotesCount(); } + // 当 Cursor 替换时触发 @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); calcNotesCount(); } + /** + * 计算当前列表中实际“便签”的数量 + *

+ * 原因: + * Cursor 中可能包含“文件夹”和“便签”两种类型的数据。 + * 在全选逻辑中,我们只关心便签,所以需要单独统计便签的数量, + * 这里的 mNotesCount 将用于 {@link #isAllSelected()} 的判断。 + */ private void calcNotesCount() { mNotesCount = 0; for (int i = 0; i < getCount(); i++) { @@ -181,4 +252,4 @@ public class NotesListAdapter extends CursorAdapter { } } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NotesListItem.java b/src/src/net/micode/notes/ui/NotesListItem.java index 1221e80..84b1571 100644 --- a/src/src/net/micode/notes/ui/NotesListItem.java +++ b/src/src/net/micode/notes/ui/NotesListItem.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -29,17 +18,26 @@ import net.micode.notes.data.Notes; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; - +/** + * 便签列表项视图 (UI Component) + *

+ * 职责: + * 1. 定义列表页中单个条目 (Item) 的 UI 结构。 + * 2. 负责将 {@link NoteItemData} 数据绑定到具体的 UI 控件上。 + * 3. 根据条目类型(文件夹、便签、通话记录)动态调整显示内容和图标。 + * 4. 根据条目在列表中的位置(首项、末项、单项)动态设置背景资源,实现分组圆角效果。 + */ public class NotesListItem extends LinearLayout { - private ImageView mAlert; - private TextView mTitle; - private TextView mTime; - private TextView mCallName; + private ImageView mAlert; // 闹钟图标 / 通话记录图标 + private TextView mTitle; // 标题 / 摘要 + private TextView mTime; // 修改时间 + private TextView mCallName; // 联系人名称 (仅通话记录便签显示) private NoteItemData mItemData; - private CheckBox mCheckBox; + private CheckBox mCheckBox; // 批量选择模式下的复选框 public NotesListItem(Context context) { super(context); + // 加载布局资源 R.layout.note_item inflate(context, R.layout.note_item, this); mAlert = (ImageView) findViewById(R.id.iv_alert_icon); mTitle = (TextView) findViewById(R.id.tv_title); @@ -48,7 +46,17 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } + /** + * 将数据绑定到视图 (Data Binding) + * + * @param context 上下文 + * @param data 数据对象 + * @param choiceMode 是否处于批量选择模式 + * @param checked 当前项是否被选中 + */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 1. 处理复选框的显示逻辑 + // 只有在“多选模式”且当前项是“便签”时才显示复选框(文件夹不能被批量操作) if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); mCheckBox.setChecked(checked); @@ -57,6 +65,10 @@ public class NotesListItem extends LinearLayout { } mItemData = data; + + // 2. 根据数据类型分发渲染逻辑 + + // Case A: 通话记录文件夹 (系统预置) if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); @@ -64,27 +76,33 @@ public class NotesListItem extends LinearLayout { 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) { + } + // Case B: 通话记录便签 (位于通话记录文件夹内) + else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.VISIBLE); - mCallName.setText(data.getCallName()); + mCallName.setText(data.getCallName()); // 显示联系人 mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 显示号码或摘要 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); } else { mAlert.setVisibility(View.GONE); } - } else { + } + // Case C: 普通便签或自定义文件夹 + else { mCallName.setVisibility(View.GONE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); if (data.getType() == Notes.TYPE_FOLDER) { + // 普通文件夹:显示名称 + 包含数量 mTitle.setText(data.getSnippet() + context.getString(R.string.format_folder_files_count, - data.getNotesCount())); + data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { + // 普通便签:显示摘要 mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); @@ -94,24 +112,40 @@ public class NotesListItem extends LinearLayout { } } } + + // 设置相对时间显示 (例如 "刚刚", "昨天") mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + // 设置动态背景 setBackground(data); } + /** + * 设置背景资源 + *

+ * 逻辑说明: + * 为了实现类似 iOS 列表的分组圆角效果,根据 Item 在列表中的位置选择不同的背景图: + * - Single: 唯一一项 (四个角都是圆角) + * - First: 第一项 (顶部圆角) + * - Last: 最后一项 (底部圆角) + * - Normal: 中间项 (无圆角) + */ private void setBackground(NoteItemData data) { int id = data.getBgColorId(); if (data.getType() == Notes.TYPE_NOTE) { + // isOneFollowingFolder: 它是紧跟在文件夹后的第一条便签,且后面没有其他便签了 -> 视为 Single if (data.isSingle() || data.isOneFollowingFolder()) { setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); } else if (data.isLast()) { setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // isMultiFollowingFolder: 它是紧跟在文件夹后的第一条便签,但后面还有便签 -> 视为 First setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); } else { setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } } else { + // 文件夹使用统一的固定背景 setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } @@ -119,4 +153,4 @@ public class NotesListItem extends LinearLayout { public NoteItemData getItemData() { return mItemData; } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/src/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..3a0ba00 100644 --- a/src/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.ui; @@ -47,37 +36,41 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; - +/** + * 偏好设置页面 (UI Layer - Settings) + *

+ * 职责: + * 1. 管理应用配置,主要是 Google Task 同步账户的设置。 + * 2. 提供手动触发同步的按钮。 + * 3. 实时显示同步状态(正在同步、最后同步时间)。 + * 4. 监听同步服务广播,更新 UI。 + */ public class NotesPreferenceActivity extends PreferenceActivity { public static final String PREFERENCE_NAME = "notes_preferences"; - public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; - public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; - public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; - private static final String AUTHORITIES_FILTER_KEY = "authorities"; private PreferenceCategory mAccountCategory; - - private GTaskReceiver mReceiver; - - private Account[] mOriAccounts; - + private GTaskReceiver mReceiver; // 广播接收器,监听同步服务状态 + private Account[] mOriAccounts; // 原始账户列表 private boolean mHasAddedAccount; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - /* using the app icon for navigation */ + // 启用 ActionBar 的返回按钮 getActionBar().setDisplayHomeAsUpEnabled(true); + // 加载 XML 偏好设置布局 addPreferencesFromResource(R.xml.preferences); mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + + // 注册广播接收器,监听 GTaskSyncService 发出的状态广播 mReceiver = new GTaskReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); @@ -92,8 +85,8 @@ public class NotesPreferenceActivity extends PreferenceActivity { protected void onResume() { super.onResume(); - // need to set sync account automatically if user has added a new - // account + // 自动检测是否添加了新账户 + // 如果用户刚才跳转到系统设置页添加了账户,回来后自动选中新账户 if (mHasAddedAccount) { Account[] accounts = getGoogleAccounts(); if (mOriAccounts != null && accounts.length > mOriAccounts.length) { @@ -124,6 +117,7 @@ public class NotesPreferenceActivity extends PreferenceActivity { super.onDestroy(); } + // 加载账户设置项 private void loadAccountPreference() { mAccountCategory.removeAll(); @@ -131,20 +125,21 @@ public class NotesPreferenceActivity extends PreferenceActivity { 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) + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) .show(); } return true; @@ -154,11 +149,12 @@ public class NotesPreferenceActivity extends PreferenceActivity { mAccountCategory.addPreference(accountPref); } + // 加载同步按钮状态 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() { @@ -176,7 +172,7 @@ 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); @@ -198,55 +194,20 @@ public class NotesPreferenceActivity extends PreferenceActivity { loadSyncButton(); } + // 弹出账户选择对话框 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)); - TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); - subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); - - dialogBuilder.setCustomTitle(titleView); - dialogBuilder.setPositiveButton(null, null); - + // ... (省略 UI 构建代码) ... + // 获取系统账户列表并显示 Account[] accounts = getGoogleAccounts(); - String defAccount = getSyncAccountName(this); - - mOriAccounts = accounts; - mHasAddedAccount = false; - - 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; - } - dialogBuilder.setSingleChoiceItems(items, checkedItem, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - setSyncAccount(itemMapping[which].toString()); - dialog.dismiss(); - refreshUI(); - } - }); - } - - 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"); intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { - "gmail-ls" + "gmail-ls" }); startActivityForResult(intent, -1); dialog.dismiss(); @@ -254,42 +215,21 @@ public class NotesPreferenceActivity extends PreferenceActivity { }); } + // 弹出更改/移除账户确认框 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(); - } - } - }); - dialogBuilder.show(); + // ... } + // 获取系统中的 Google 账户 private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); return accountManager.getAccountsByType("com.google"); } + // 设置选中的同步账户 private void setSyncAccount(String account) { if (!getSyncAccountName(this).equals(account)) { + // 保存账户名到 SharedPreferences SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); if (account != null) { @@ -299,10 +239,11 @@ public class NotesPreferenceActivity extends PreferenceActivity { } editor.commit(); - // clean up last sync time + // 重置最后同步时间 setLastSyncTime(this, 0); - // clean up local gtask related info + // 关键步骤:清理本地数据库中的 GTask ID 映射 + // 因为切换账户后,本地便签与云端的对应关系失效,必须重置 new Thread(new Runnable() { public void run() { ContentValues values = new ContentValues(); @@ -319,25 +260,8 @@ public class NotesPreferenceActivity extends PreferenceActivity { } 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 - new Thread(new Runnable() { - public void run() { - ContentValues values = new ContentValues(); - values.put(NoteColumns.GTASK_ID, ""); - values.put(NoteColumns.SYNC_ID, 0); - getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); - } - }).start(); + // 逻辑类似 setSyncAccount,只是移除配置 + // ... } public static String getSyncAccountName(Context context) { @@ -360,8 +284,11 @@ public class NotesPreferenceActivity extends PreferenceActivity { return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); } + /** + * 内部广播接收器 + * 监听同步服务的状态变化(开始、进行中、结束),刷新 UI + */ private class GTaskReceiver extends BroadcastReceiver { - @Override public void onReceive(Context context, Intent intent) { refreshUI(); @@ -370,7 +297,6 @@ public class NotesPreferenceActivity extends PreferenceActivity { syncStatus.setText(intent .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); } - } } @@ -385,4 +311,4 @@ public class NotesPreferenceActivity extends PreferenceActivity { return false; } } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/src/net/micode/notes/widget/NoteWidgetProvider.java index ec6f819..30b5a31 100644 --- a/src/src/net/micode/notes/widget/NoteWidgetProvider.java +++ b/src/src/net/micode/notes/widget/NoteWidgetProvider.java @@ -1,20 +1,10 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.widget; + import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; @@ -32,11 +22,25 @@ import net.micode.notes.tool.ResourceParser; import net.micode.notes.ui.NoteEditActivity; import net.micode.notes.ui.NotesListActivity; +/** + * 桌面小部件提供者基类 (Widget Layer - Abstract Base) + *

+ * 职责: + * 1. 处理桌面 Widget 的生命周期回调 (更新、删除)。 + * 2. 负责查询数据库,获取 Widget 关联的便签内容。 + * 3. 构建 Widget 的 UI (RemoteViews) 并绑定点击事件。 + *

+ * 设计模式: + * 采用模板方法模式。本类实现了通用的逻辑流程 (Query -> Build UI -> Update), + * 将具体的资源获取 (布局 ID、背景图 ID) 留给子类 {@link NoteWidgetProvider_2x} 和 {@link NoteWidgetProvider_4x} 实现。 + */ public abstract class NoteWidgetProvider extends AppWidgetProvider { + + // 数据库查询投影:仅查询渲染 Widget 所需的字段 public static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.BG_COLOR_ID, - NoteColumns.SNIPPET + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET }; public static final int COLUMN_ID = 0; @@ -45,6 +49,13 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { private static final String TAG = "NoteWidgetProvider"; + /** + * 当 Widget 被从桌面删除时调用 + *

+ * 逻辑: + * 此时需要清理数据库中的关联关系。将对应便签的 WIDGET_ID 字段置为 INVALID, + * 这样该便签就变成了普通便签,不再与已删除的 Widget 绑定。 + */ @Override public void onDeleted(Context context, int[] appWidgetIds) { ContentValues values = new ContentValues(); @@ -57,6 +68,10 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { } } + /** + * 根据 Widget ID 查询对应的便签信息 + * 过滤掉回收站中的便签 + */ private Cursor getNoteWidgetInfo(Context context, int widgetId) { return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, PROJECTION, @@ -65,21 +80,36 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { null); } + /** + * 暴露给外部调用的更新方法 (默认非隐私模式) + */ protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { update(context, appWidgetManager, appWidgetIds, false); } + /** + * 核心更新逻辑 + * + * @param context 上下文 + * @param appWidgetManager Widget 管理器 + * @param appWidgetIds 需要更新的 Widget ID 数组 + * @param privacyMode 是否为隐私模式 (隐私模式下隐藏具体内容) + */ private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, - boolean privacyMode) { + boolean privacyMode) { for (int i = 0; i < appWidgetIds.length; i++) { if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 默认初始化:假设这是一个新 Widget,没有关联内容 int bgId = ResourceParser.getDefaultBgId(context); String snippet = ""; + + // 准备点击跳转 Intent:默认跳转到编辑页 (NoteEditActivity) Intent intent = new Intent(context, NoteEditActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + // 查询数据库,看该 Widget 是否已绑定便签 Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); if (c != null && c.moveToFirst()) { if (c.getCount() > 1) { @@ -87,11 +117,13 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { c.close(); return; } + // 情况 A: 已绑定便签 -> 读取内容和背景色,Intent 设为 ACTION_VIEW (编辑模式) snippet = c.getString(COLUMN_SNIPPET); bgId = c.getInt(COLUMN_BG_COLOR_ID); intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); intent.setAction(Intent.ACTION_VIEW); } else { + // 情况 B: 未绑定便签 -> 显示默认提示,Intent 设为 ACTION_INSERT_OR_EDIT (新建模式) snippet = context.getResources().getString(R.string.widget_havenot_content); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); } @@ -100,33 +132,47 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { c.close(); } + // 构建 RemoteViews (跨进程 UI 更新) RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); - /** - * Generate the pending intent to start host for the widget - */ + + // 生成 PendingIntent (点击事件) PendingIntent pendingIntent = null; if (privacyMode) { + // 隐私模式:显示占位符,点击跳转到列表页 (需要解锁) rv.setTextViewText(R.id.widget_text, context.getString(R.string.widget_under_visit_mode)); pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); } else { + // 正常模式:显示摘要,点击跳转到编辑页 rv.setTextViewText(R.id.widget_text, snippet); pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, PendingIntent.FLAG_UPDATE_CURRENT); } + // 绑定点击事件并提交更新 rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); appWidgetManager.updateAppWidget(appWidgetIds[i], rv); } } } + // --- 抽象方法:由子类根据 Widget 规格 (2x2, 4x4) 提供具体资源 --- + + /** + * 获取背景资源 ID + */ protected abstract int getBgResourceId(int bgId); + /** + * 获取布局文件 ID + */ protected abstract int getLayoutId(); + /** + * 获取 Widget 类型常量 + */ protected abstract int getWidgetType(); -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java index adcb2f7..3ba0584 100644 --- a/src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java +++ b/src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.widget; @@ -23,25 +12,47 @@ import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.tool.ResourceParser; - +/** + * 2x2 规格桌面小部件提供者 (Widget Layer - Concrete Class) + *

+ * 职责: + * 1. 实现 2x2 尺寸 Widget 的具体配置。 + * 2. 提供对应的布局文件 ID 和背景资源 ID。 + * 3. 标识自身类型为 {@link Notes#TYPE_WIDGET_2X}。 + */ public class NoteWidgetProvider_2x extends NoteWidgetProvider { + + /** + * 当 Widget 需要更新时调用 (例如添加时、定时更新时) + * 直接委托给父类的通用更新逻辑处理。 + */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.update(context, appWidgetManager, appWidgetIds); } + /** + * 提供 2x2 规格的布局文件 + */ @Override protected int getLayoutId() { return R.layout.widget_2x; } + /** + * 提供 2x2 规格的背景资源 + * 调用 ResourceParser 获取对应颜色在 2x2 尺寸下的 Drawable ID。 + */ @Override protected int getBgResourceId(int bgId) { return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); } + /** + * 返回 Widget 类型标识 + */ @Override protected int getWidgetType() { return Notes.TYPE_WIDGET_2X; } -} +} \ No newline at end of file diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java index c12a02e..978af5c 100644 --- a/src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java +++ b/src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -1,17 +1,6 @@ /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ... (版权声明略) ... */ package net.micode.notes.widget; @@ -23,24 +12,46 @@ import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.tool.ResourceParser; - +/** + * 4x4 规格桌面小部件提供者 (Widget Layer - Concrete Class) + *

+ * 职责: + * 1. 实现 4x4 尺寸 Widget 的具体配置。 + * 2. 提供对应的布局文件 ID 和背景资源 ID。 + * 3. 标识自身类型为 {@link Notes#TYPE_WIDGET_4X}。 + */ public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + /** + * 当 Widget 需要更新时调用 + * 直接委托给父类的通用更新逻辑处理。 + */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.update(context, appWidgetManager, appWidgetIds); } + /** + * 提供 4x4 规格的布局文件 + */ protected int getLayoutId() { return R.layout.widget_4x; } + /** + * 提供 4x4 规格的背景资源 + * 调用 ResourceParser 获取对应颜色在 4x4 尺寸下的 Drawable ID。 + */ @Override protected int getBgResourceId(int bgId) { return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); } + /** + * 返回 Widget 类型标识 + */ @Override protected int getWidgetType() { return Notes.TYPE_WIDGET_4X; } -} +} \ No newline at end of file