From 9d4fb1a67a6eb72822ff478ede2b83e7fd0c6980 Mon Sep 17 00:00:00 2001 From: fanshuang <2963071932qq.com> Date: Thu, 26 Dec 2024 16:34:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=AF=B9gtask=E5=8C=85=E7=9A=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/net/micode/notes/gtask/data/MetaData.java | 84 ++- src/net/micode/notes/gtask/data/Node.java | 258 +++++--- src/net/micode/notes/gtask/data/SqlData.java | 125 ++-- src/net/micode/notes/gtask/data/SqlNote.java | 393 ++++++++---- src/net/micode/notes/gtask/data/Task.java | 366 ++++++----- src/net/micode/notes/gtask/data/TaskList.java | 443 ++++++++----- .../exception/ActionFailureException.java | 46 +- .../exception/NetworkFailureException.java | 46 +- .../notes/gtask/remote/GTaskASyncTask.java | 110 ++-- .../notes/gtask/remote/GTaskClient.java | 404 ++++++------ .../notes/gtask/remote/GTaskManager.java | 603 ++++++------------ .../notes/gtask/remote/GTaskSyncService.java | 116 +++- 12 files changed, 1705 insertions(+), 1289 deletions(-) diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java index 3a2050b..60ef9ff 100644 --- a/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -1,20 +1,21 @@ -/* - * 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. - */ +```java + /* + * 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.gtask.data; + package net.micode.notes.gtask.data; import android.database.Cursor; import android.util.Log; @@ -24,14 +25,27 @@ import net.micode.notes.tool.GTaskStringUtils; import org.json.JSONException; import org.json.JSONObject; - +/** + * MetaData 类继承自 Task,用于处理与任务相关的元数据。 + * 主要功能包括设置和获取与任务关联的全局唯一标识符(GID)。 + */ public class MetaData extends Task { private final static String TAG = MetaData.class.getSimpleName(); + /** + * 存储与任务关联的全局唯一标识符(GID)。 + */ private String mRelatedGid = null; + /** + * 设置任务的元数据信息,并将 GID 添加到元数据中。 + * + * @param gid 任务的全局唯一标识符(GID) + * @param metaInfo 包含任务元数据的 JSON 对象 + */ public void setMeta(String gid, JSONObject metaInfo) { try { + // 将 GID 添加到元数据中 metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); } catch (JSONException e) { Log.e(TAG, "failed to put related gid"); @@ -40,20 +54,36 @@ public class MetaData extends Task { setName(GTaskStringUtils.META_NOTE_NAME); } + /** + * 获取与任务关联的全局唯一标识符(GID)。 + * + * @return 返回任务的 GID + */ public String getRelatedGid() { return mRelatedGid; } + /** + * 判断是否有值得保存的笔记内容。 + * + * @return 如果有笔记内容则返回 true,否则返回 false + */ @Override public boolean isWorthSaving() { return getNotes() != null; } + /** + * 通过远程 JSON 数据设置任务内容,并解析出 GID。 + * + * @param js 包含任务内容的 JSON 对象 + */ @Override public void setContentByRemoteJSON(JSONObject js) { super.setContentByRemoteJSON(js); if (getNotes() != null) { try { + // 解析笔记内容中的元数据并提取 GID JSONObject metaInfo = new JSONObject(getNotes().trim()); mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); } catch (JSONException e) { @@ -63,20 +93,36 @@ public class MetaData extends Task { } } + /** + * 本地 JSON 数据设置任务内容的方法不应被调用。 + * + * @param js 包含任务内容的 JSON 对象 + * @throws IllegalAccessError 抛出异常表示该方法不应被调用 + */ @Override public void setContentByLocalJSON(JSONObject js) { - // this function should not be called throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); } + /** + * 从任务内容生成本地 JSON 数据的方法不应被调用。 + * + * @throws IllegalAccessError 抛出异常表示该方法不应被调用 + */ @Override public JSONObject getLocalJSONFromContent() { throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } + /** + * 获取同步操作的方法不应被调用。 + * + * @param c 游标对象 + * @throws IllegalAccessError 抛出异常表示该方法不应被调用 + */ @Override public int getSyncAction(Cursor c) { throw new IllegalAccessError("MetaData:getSyncAction should not be called"); } - } +``` diff --git a/src/net/micode/notes/gtask/data/Node.java b/src/net/micode/notes/gtask/data/Node.java index 63950e0..b6c5785 100644 --- a/src/net/micode/notes/gtask/data/Node.java +++ b/src/net/micode/notes/gtask/data/Node.java @@ -21,81 +21,195 @@ import android.database.Cursor; import org.json.JSONObject; public abstract class Node { - public static final int SYNC_ACTION_NONE = 0; - public static final int SYNC_ACTION_ADD_REMOTE = 1; - - public static final int SYNC_ACTION_ADD_LOCAL = 2; - - public static final int SYNC_ACTION_DEL_REMOTE = 3; - - public static final int SYNC_ACTION_DEL_LOCAL = 4; - - public static final int SYNC_ACTION_UPDATE_REMOTE = 5; - - public static final int SYNC_ACTION_UPDATE_LOCAL = 6; - - public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; - - public static final int SYNC_ACTION_ERROR = 8; - - private String mGid; - - private String mName; - - private long mLastModified; - - private boolean mDeleted; - - public Node() { - mGid = null; - mName = ""; - mLastModified = 0; - mDeleted = false; - } - - public abstract JSONObject getCreateAction(int actionId); - - public abstract JSONObject getUpdateAction(int actionId); - - public abstract void setContentByRemoteJSON(JSONObject js); - - public abstract void setContentByLocalJSON(JSONObject js); - - public abstract JSONObject getLocalJSONFromContent(); - - public abstract int getSyncAction(Cursor c); - - public void setGid(String gid) { - this.mGid = gid; - } - - public void setName(String name) { - this.mName = name; - } - - public void setLastModified(long lastModified) { - this.mLastModified = lastModified; - } - - public void setDeleted(boolean deleted) { - this.mDeleted = deleted; - } - - public String getGid() { - return this.mGid; - } +package net.micode.notes.gtask.data; - public String getName() { - return this.mName; - } +import android.database.Cursor; - public long getLastModified() { - return this.mLastModified; - } +import org.json.JSONObject; - public boolean getDeleted() { - return this.mDeleted; + /** + * 表示任务节点的抽象类,用于同步和管理任务数据。 + * 包含了与任务相关的属性和方法,支持本地和远程数据的同步操作。 + */ + public abstract class Node { + + /** + * 同步动作:无操作 + */ + public static final int SYNC_ACTION_NONE = 0; + + /** + * 同步动作:远程添加 + */ + public static final int SYNC_ACTION_ADD_REMOTE = 1; + + /** + * 同步动作:本地添加 + */ + public static final int SYNC_ACTION_ADD_LOCAL = 2; + + /** + * 同步动作:远程删除 + */ + public static final int SYNC_ACTION_DEL_REMOTE = 3; + + /** + * 同步动作:本地删除 + */ + public static final int SYNC_ACTION_DEL_LOCAL = 4; + + /** + * 同步动作:远程更新 + */ + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; + + /** + * 同步动作:本地更新 + */ + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; + + /** + * 同步动作:更新冲突 + */ + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; + + /** + * 同步动作:错误 + */ + public static final int SYNC_ACTION_ERROR = 8; + + private String mGid; // 任务的全局唯一标识符 + private String mName; // 任务名称 + private long mLastModified; // 最后修改时间 + private boolean mDeleted; // 是否已删除 + + /** + * 构造函数,初始化任务节点的基本属性。 + */ + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } + + /** + * 获取创建操作的 JSON 对象。 + * + * @param actionId 操作类型 ID + * @return 创建操作的 JSON 对象 + */ + public abstract JSONObject getCreateAction(int actionId); + + /** + * 获取更新操作的 JSON 对象。 + * + * @param actionId 操作类型 ID + * @return 更新操作的 JSON 对象 + */ + public abstract JSONObject getUpdateAction(int actionId); + + /** + * 根据远程 JSON 数据设置任务内容。 + * + * @param js 远程 JSON 数据 + */ + public abstract void setContentByRemoteJSON(JSONObject js); + + /** + * 根据本地 JSON 数据设置任务内容。 + * + * @param js 本地 JSON 数据 + */ + public abstract void setContentByLocalJSON(JSONObject js); + + /** + * 将任务内容转换为本地 JSON 对象。 + * + * @return 任务内容的本地 JSON 对象 + */ + public abstract JSONObject getLocalJSONFromContent(); + + /** + * 根据数据库游标获取同步操作类型。 + * + * @param c 数据库游标 + * @return 同步操作类型 + */ + public abstract int getSyncAction(Cursor c); + + /** + * 设置任务的全局唯一标识符。 + * + * @param gid 全局唯一标识符 + */ + public void setGid(String gid) { + this.mGid = gid; + } + + /** + * 设置任务名称。 + * + * @param name 任务名称 + */ + public void setName(String name) { + this.mName = name; + } + + /** + * 设置任务的最后修改时间。 + * + * @param lastModified 最后修改时间 + */ + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + /** + * 设置任务是否已删除。 + * + * @param deleted 是否已删除 + */ + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + /** + * 获取任务的全局唯一标识符。 + * + * @return 全局唯一标识符 + */ + public String getGid() { + return this.mGid; + } + + /** + * 获取任务名称。 + * + * @return 任务名称 + */ + public String getName() { + return this.mName; + } + + /** + * 获取任务的最后修改时间。 + * + * @return 最后修改时间 + */ + public long getLastModified() { + return this.mLastModified; + } + + /** + * 获取任务是否已删除。 + * + * @return 是否已删除 + */ + public boolean getDeleted() { + return this.mDeleted; + } } -} +} \ No newline at end of file diff --git a/src/net/micode/notes/gtask/data/SqlData.java b/src/net/micode/notes/gtask/data/SqlData.java index d3ec3be..d17bf8a 100644 --- a/src/net/micode/notes/gtask/data/SqlData.java +++ b/src/net/micode/notes/gtask/data/SqlData.java @@ -1,20 +1,4 @@ -/* - * 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.gtask.data; + package net.micode.notes.gtask.data; import android.content.ContentResolver; import android.content.ContentUris; @@ -28,49 +12,67 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.NotesDatabaseHelper.TABLE; import net.micode.notes.gtask.exception.ActionFailureException; import org.json.JSONException; import org.json.JSONObject; - +/** + * SqlData 类用于处理与数据库交互的任务数据。 + * 包含了从 JSON 对象加载数据、将数据保存到数据库以及获取和设置任务数据的方法。 + */ public class SqlData { - private static final String TAG = SqlData.class.getSimpleName(); + private static final String TAG = SqlData.class.getSimpleName(); // 日志标签 - private static final int INVALID_ID = -99999; + private static final int INVALID_ID = -99999; // 无效的 ID,表示未初始化或无效的数据 + /** + * 数据库查询投影,包含需要查询的列名。 + */ public static final String[] PROJECTION_DATA = new String[] { DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, DataColumns.DATA3 }; + /** + * 数据 ID 列索引 + */ public static final int DATA_ID_COLUMN = 0; + /** + * MIME 类型列索引 + */ public static final int DATA_MIME_TYPE_COLUMN = 1; + /** + * 内容列索引 + */ public static final int DATA_CONTENT_COLUMN = 2; + /** + * 数据1列索引 + */ public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + /** + * 数据3列索引 + */ public static final int DATA_CONTENT_DATA_3_COLUMN = 4; - private ContentResolver mContentResolver; - - private boolean mIsCreate; - - private long mDataId; - - private String mDataMimeType; - - private String mDataContent; - - private long mDataContentData1; - - private String mDataContentData3; - - private ContentValues mDiffDataValues; - + private ContentResolver mContentResolver; // 内容解析器,用于与内容提供者交互 + private boolean mIsCreate; // 标记是否为创建操作 + private long mDataId; // 数据 ID + private String mDataMimeType; // 数据 MIME 类型 + private String mDataContent; // 数据内容 + private long mDataContentData1; // 数据字段1 + private String mDataContentData3; // 数据字段3 + private ContentValues mDiffDataValues; // 用于存储要更新的字段值 + + /** + * 构造函数,初始化一个新的 SqlData 实例。 + * + * @param context 上下文对象 + */ public SqlData(Context context) { mContentResolver = context.getContentResolver(); mIsCreate = true; @@ -82,6 +84,12 @@ public class SqlData { mDiffDataValues = new ContentValues(); } + /** + * 构造函数,从游标加载现有数据并初始化 SqlData 实例。 + * + * @param context 上下文对象 + * @param c 游标对象 + */ public SqlData(Context context, Cursor c) { mContentResolver = context.getContentResolver(); mIsCreate = false; @@ -89,6 +97,11 @@ public class SqlData { mDiffDataValues = new ContentValues(); } + /** + * 从游标中加载数据。 + * + * @param c 游标对象 + */ private void loadFromCursor(Cursor c) { mDataId = c.getLong(DATA_ID_COLUMN); mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); @@ -97,6 +110,12 @@ public class SqlData { mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); } + /** + * 从 JSON 对象加载数据,并记录差异。 + * + * @param js JSON 对象 + * @throws JSONException 如果解析 JSON 失败 + */ public void setContent(JSONObject js) throws JSONException { long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; if (mIsCreate || mDataId != dataId) { @@ -130,9 +149,15 @@ public class SqlData { mDataContentData3 = dataContentData3; } + /** + * 将当前数据转换为 JSON 对象。 + * + * @return JSON 对象 + * @throws JSONException 如果生成 JSON 失败 + */ public JSONObject getContent() throws JSONException { if (mIsCreate) { - Log.e(TAG, "it seems that we haven't created this in database yet"); + Log.e(TAG, "似乎我们还没有在数据库中创建此数据"); return null; } JSONObject js = new JSONObject(); @@ -144,8 +169,14 @@ public class SqlData { return js; } + /** + * 将数据提交到数据库。 + * + * @param noteId 笔记 ID + * @param validateVersion 是否验证版本号 + * @param version 版本号 + */ public void commit(long noteId, boolean validateVersion, long version) { - if (mIsCreate) { if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { mDiffDataValues.remove(DataColumns.ID); @@ -156,8 +187,8 @@ public class SqlData { try { mDataId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { - Log.e(TAG, "Get note id error :" + e.toString()); - throw new ActionFailureException("create note failed"); + Log.e(TAG, "获取笔记 ID 错误:" + e.toString()); + throw new ActionFailureException("创建笔记失败"); } } else { if (mDiffDataValues.size() > 0) { @@ -167,14 +198,14 @@ public class SqlData { Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); } else { result = mContentResolver.update(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, - " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, + " ? in (SELECT " + NoteColumns.ID + " FROM " + Notes.TABLE_NAME + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { String.valueOf(noteId), String.valueOf(version) }); } if (result == 0) { - Log.w(TAG, "there is no update. maybe user updates note when syncing"); + Log.w(TAG, "没有更新。可能是用户在同步时修改了笔记"); } } } @@ -183,7 +214,13 @@ public class SqlData { mIsCreate = false; } + /** + * 获取数据 ID。 + * + * @return 数据 ID + */ public long getId() { return mDataId; } } + diff --git a/src/net/micode/notes/gtask/data/SqlNote.java b/src/net/micode/notes/gtask/data/SqlNote.java index 79a4095..d7f8ece 100644 --- a/src/net/micode/notes/gtask/data/SqlNote.java +++ b/src/net/micode/notes/gtask/data/SqlNote.java @@ -1,49 +1,29 @@ -/* - * 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.gtask.data; - -import android.appwidget.AppWidgetManager; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.data.SqlData; import net.micode.notes.gtask.exception.ActionFailureException; import net.micode.notes.tool.GTaskStringUtils; import net.micode.notes.tool.ResourceParser; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; - - +import javax.naming.Context; +import java.awt.*; +import java.util.ArrayList; /** + * SqlNote 类用于表示和操作笔记对象,包括创建、更新和查询笔记数据。 + */ public class SqlNote { + + /** + * 日志标签,用于调试和日志记录。 + */ private static final String TAG = SqlNote.class.getSimpleName(); + /** + * 无效 ID 的常量值。 + */ private static final int INVALID_ID = -99999; - public static final String[] PROJECTION_NOTE = new String[] { + /** + * 笔记的投影字段数组,定义了从数据库中查询笔记时需要的列。 + */ + public static final String[] PROJECTION_NOTE = 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, @@ -52,76 +32,122 @@ public class SqlNote { NoteColumns.VERSION }; + /** + * 定义投影字段数组中各列的索引位置。 + */ public static final int ID_COLUMN = 0; - public static final int ALERTED_DATE_COLUMN = 1; - public static final int BG_COLOR_ID_COLUMN = 2; - public static final int CREATED_DATE_COLUMN = 3; - public static final int HAS_ATTACHMENT_COLUMN = 4; - public static final int MODIFIED_DATE_COLUMN = 5; - public static final int NOTES_COUNT_COLUMN = 6; - public static final int PARENT_ID_COLUMN = 7; - public static final int SNIPPET_COLUMN = 8; - public static final int TYPE_COLUMN = 9; - public static final int WIDGET_ID_COLUMN = 10; - public static final int WIDGET_TYPE_COLUMN = 11; - public static final int SYNC_ID_COLUMN = 12; - public static final int LOCAL_MODIFIED_COLUMN = 13; - public static final int ORIGIN_PARENT_ID_COLUMN = 14; - public static final int GTASK_ID_COLUMN = 15; - public static final int VERSION_COLUMN = 16; + /** + * 上下文对象,用于访问应用程序资源。 + */ private Context mContext; + /** + * 内容解析器,用于与内容提供者进行交互。 + */ private ContentResolver mContentResolver; + /** + * 标记是否是新创建的笔记。 + */ private boolean mIsCreate; + /** + * 笔记的唯一标识符。 + */ private long mId; + /** + * 提醒日期。 + */ private long mAlertDate; + /** + * 背景颜色 ID。 + */ private int mBgColorId; + /** + * 创建日期。 + */ private long mCreatedDate; + /** + * 是否包含附件。 + */ private int mHasAttachment; + /** + * 修改日期。 + */ private long mModifiedDate; + /** + * 父级笔记 ID。 + */ private long mParentId; + /** + * 笔记片段。 + */ private String mSnippet; + /** + * 笔记类型。 + */ private int mType; + /** + * 小部件 ID。 + */ private int mWidgetId; + /** + * 小部件类型。 + */ private int mWidgetType; + /** + * 原始父级笔记 ID。 + */ private long mOriginParent; + /** + * 笔记版本号。 + */ private long mVersion; + /** + * 存储差异笔记值的 ContentValues 对象。 + */ private ContentValues mDiffNoteValues; + /** + * 存储笔记数据的列表。 + */ private ArrayList mDataList; + /** + * 构造函数,用于创建新的 SqlNote 实例。 + * + * @param context 应用程序上下文。 + */ public SqlNote(Context context) { mContext = context; mContentResolver = context.getContentResolver(); @@ -143,6 +169,12 @@ public class SqlNote { mDataList = new ArrayList(); } + /** + * 构造函数,用于从游标加载现有笔记数据。 + * + * @param context 应用程序上下文。 + * @param c 包含笔记数据的游标。 + */ public SqlNote(Context context, Cursor c) { mContext = context; mContentResolver = context.getContentResolver(); @@ -154,6 +186,12 @@ public class SqlNote { mDiffNoteValues = new ContentValues(); } + /** + * 构造函数,用于通过笔记 ID 加载现有笔记数据。 + * + * @param context 应用程序上下文。 + * @param id 笔记的唯一标识符。 + */ public SqlNote(Context context, long id) { mContext = context; mContentResolver = context.getContentResolver(); @@ -163,16 +201,18 @@ public class SqlNote { if (mType == Notes.TYPE_NOTE) loadDataContent(); mDiffNoteValues = new ContentValues(); - } + /** + * 通过笔记 ID 从内容提供者加载笔记数据。 + * + * @param id 笔记的唯一标识符。 + */ private void loadFromCursor(long id) { Cursor c = null; try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(id) - }, null); + new String[]{String.valueOf(id)}, null); if (c != null) { c.moveToNext(); loadFromCursor(c); @@ -185,6 +225,11 @@ public class SqlNote { } } + /** + * 从游标加载笔记数据。 + * + * @param c 包含笔记数据的游标。 + */ private void loadFromCursor(Cursor c) { mId = c.getLong(ID_COLUMN); mAlertDate = c.getLong(ALERTED_DATE_COLUMN); @@ -200,14 +245,15 @@ public class SqlNote { mVersion = c.getLong(VERSION_COLUMN); } + /** + * 加载笔记的内容数据。 + */ private void loadDataContent() { Cursor c = null; mDataList.clear(); try { c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, - "(note_id=?)", new String[] { - String.valueOf(mId) - }, null); + "(note_id=?)", new String[]{String.valueOf(mId)}, null); if (c != null) { if (c.getCount() == 0) { Log.w(TAG, "it seems that the note has not data"); @@ -226,22 +272,26 @@ public class SqlNote { } } + /** + * 设置笔记内容,从 JSON 对象中解析并更新笔记属性。 + * + * @param js 包含笔记内容的 JSON 对象。 + * @return 返回设置是否成功。 + */ public boolean setContent(JSONObject js) { try { JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { Log.w(TAG, "cannot set system folder"); } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { - // for folder we can only update the snnipet and type - String snippet = note.has(NoteColumns.SNIPPET) ? note - .getString(NoteColumns.SNIPPET) : ""; + // 对于文件夹,我们只能更新片段和类型 + String snippet = note.has(NoteColumns.SNIPPET) ? note.getString(NoteColumns.SNIPPET) : ""; if (mIsCreate || !mSnippet.equals(snippet)) { mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); } mSnippet = snippet; - int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) - : Notes.TYPE_NOTE; + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE; if (mIsCreate || mType != type) { mDiffNoteValues.put(NoteColumns.TYPE, type); } @@ -254,78 +304,67 @@ public class SqlNote { } mId = id; - long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note - .getLong(NoteColumns.ALERTED_DATE) : 0; + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note.getLong(NoteColumns.ALERTED_DATE) : 0; if (mIsCreate || mAlertDate != alertDate) { mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); } mAlertDate = alertDate; - int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note - .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); if (mIsCreate || mBgColorId != bgColorId) { mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); } mBgColorId = bgColorId; - long createDate = note.has(NoteColumns.CREATED_DATE) ? note - .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + long createDate = note.has(NoteColumns.CREATED_DATE) ? note.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); if (mIsCreate || mCreatedDate != createDate) { mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); } mCreatedDate = createDate; - int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note - .getInt(NoteColumns.HAS_ATTACHMENT) : 0; + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note.getInt(NoteColumns.HAS_ATTACHMENT) : 0; if (mIsCreate || mHasAttachment != hasAttachment) { mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); } mHasAttachment = hasAttachment; - long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note - .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); if (mIsCreate || mModifiedDate != modifiedDate) { mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); } mModifiedDate = modifiedDate; - long parentId = note.has(NoteColumns.PARENT_ID) ? note - .getLong(NoteColumns.PARENT_ID) : 0; + long parentId = note.has(NoteColumns.PARENT_ID) ? note.getLong(NoteColumns.PARENT_ID) : 0; if (mIsCreate || mParentId != parentId) { mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); } mParentId = parentId; - String snippet = note.has(NoteColumns.SNIPPET) ? note - .getString(NoteColumns.SNIPPET) : ""; + String snippet = note.has(NoteColumns.SNIPPET) ? note.getString(NoteColumns.SNIPPET) : ""; if (mIsCreate || !mSnippet.equals(snippet)) { mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); } mSnippet = snippet; - int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) - : Notes.TYPE_NOTE; + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE; if (mIsCreate || mType != type) { mDiffNoteValues.put(NoteColumns.TYPE, type); } mType = type; - int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) - : AppWidgetManager.INVALID_APPWIDGET_ID; + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) : AppWidgetManager.INVALID_APPWIDGET_ID; if (mIsCreate || mWidgetId != widgetId) { mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); } mWidgetId = widgetId; - int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note - .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; if (mIsCreate || mWidgetType != widgetType) { mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); } mWidgetType = widgetType; - long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note - .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; if (mIsCreate || mOriginParent != originParent) { mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); } @@ -359,6 +398,11 @@ public class SqlNote { return true; } + /** + * 获取笔记内容,将笔记属性转换为 JSON 对象。 + * + * @return 返回包含笔记内容的 JSON 对象。 + */ public JSONObject getContent() { try { JSONObject js = new JSONObject(); @@ -407,99 +451,170 @@ public class SqlNote { return null; } + /** + * 设置笔记的父级 ID。 + * + * @param id 父级笔记的唯一标识符。 + */ public void setParentId(long id) { mParentId = id; mDiffNoteValues.put(NoteColumns.PARENT_ID, id); } + /** + * 设置笔记的 GTask ID。 + * + * @param gid GTask ID。 + */ public void setGtaskId(String gid) { mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); } + /** + * 设置笔记的同步 ID。 + * + * @param syncId 同步 ID。 + */ public void setSyncId(long syncId) { mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); } + /** + * 重置本地修改标记。 + */ public void resetLocalModified() { mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); } + /** + * 获取笔记的唯一标识符。 + * + * @return 返回笔记的唯一标识符。 + */ public long getId() { return mId; } + /** + * 获取笔记的父级 ID。 + * + * @return 返回父级笔记的唯一标识符。 + */ public long getParentId() { return mParentId; } + /** + * 获取笔记片段。 + * + * @return 返回笔记片段。 + */ public String getSnippet() { return mSnippet; } + /** + * 检查笔记是否为普通笔记类型。 + * + * @return 如果是普通笔记类型返回 true,否则返回 false。 + */ public boolean isNoteType() { return mType == Notes.TYPE_NOTE; } +/** + * 提交笔记更改到内容提供者。 + * + * @param validateVersion 是否验证版本号。 + */ +public void commit(boolean validateVersion) { + if (mIsCreate) { + // 如果是新创建的笔记,插入数据并获取生成的 ID + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } - public void commit(boolean validateVersion) { - if (mIsCreate) { - if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { - mDiffNoteValues.remove(NoteColumns.ID); - } + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "获取笔记 ID 错误: " + e.toString()); + throw new ActionFailureException("创建笔记失败"); + } + if (mId == 0) { + throw new IllegalStateException("创建线程 ID 失败"); + } - Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); - try { - mId = Long.valueOf(uri.getPathSegments().get(1)); - } catch (NumberFormatException e) { - Log.e(TAG, "Get note id error :" + e.toString()); - throw new ActionFailureException("create note failed"); - } - if (mId == 0) { - throw new IllegalStateException("Create thread id failed"); + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, false, -1); } + } + } else { + // 检查笔记 ID 是否有效 + if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { + Log.e(TAG, "不存在此笔记"); + throw new IllegalStateException("尝试使用无效 ID 更新笔记"); + } - if (mType == Notes.TYPE_NOTE) { - for (SqlData sqlData : mDataList) { - sqlData.commit(mId, false, -1); - } - } - } else { - if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { - Log.e(TAG, "No such note"); - throw new IllegalStateException("Try to update note with invalid id"); + // 如果有差异值需要更新 + if (mDiffNoteValues.size() > 0) { + mVersion++; + int result = 0; + if (!validateVersion) { + // 不验证版本号时直接更新笔记 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, + "(" + NoteColumns.ID + "=?)", + new String[]{String.valueOf(mId)}); + } else { + // 验证版本号时更新笔记 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, + "(" + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[]{String.valueOf(mId), String.valueOf(mVersion)}); } - if (mDiffNoteValues.size() > 0) { - mVersion ++; - int result = 0; - if (!validateVersion) { - result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?)", new String[] { - String.valueOf(mId) - }); - } else { - result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", - new String[] { - String.valueOf(mId), String.valueOf(mVersion) - }); - } - if (result == 0) { - Log.w(TAG, "there is no update. maybe user updates note when syncing"); - } + + if (result == 0) { + Log.w(TAG, "没有进行任何更新。可能是用户在同步时修改了笔记"); } + } - if (mType == Notes.TYPE_NOTE) { - for (SqlData sqlData : mDataList) { - sqlData.commit(mId, validateVersion, mVersion); - } + // 如果是普通笔记类型,提交笔记内容数据 + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); } } + } - // refresh local info - loadFromCursor(mId); - if (mType == Notes.TYPE_NOTE) - loadDataContent(); + // 刷新本地信息 + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); - mDiffNoteValues.clear(); - mIsCreate = false; - } + // 清空差异值,并标记为已创建 + mDiffNoteValues.clear(); + mIsCreate = false; } +} + + 注释解释 + +1. 检查笔记 ID 是否有效: + - 如果笔记 ID 小于等于 0 且不是根文件夹或通话记录文件夹,则抛出异常,提示尝试使用无效 ID 更新笔记。 + +2. 更新笔记数据: + - 如果 `mDiffNoteValues` 中有需要更新的数据,则增加版本号 `mVersion`。 + - 根据是否验证版本号,选择不同的更新条件: + - 不验证版本号时,直接根据笔记 ID 更新。 + - 验证版本号时,确保笔记版本号小于等于当前版本号。 + - 记录更新结果,如果没有任何更新,日志警告可能用户在同步时修改了笔记。 + +3. 提交笔记内容数据: + - 如果是普通笔记类型(`Notes.TYPE_NOTE`),则遍历 `mDataList` 并提交每个 `SqlData` 的内容。 + +4. 刷新本地信息: + - 通过 `loadFromCursor` 方法重新加载笔记数据。 + - 如果是普通笔记类型,加载笔记的内容数据。 + +5. 清理和标记: + - 清空 `mDiffNoteValues`,防止重复提交。 + - 将 `mIsCreate` 标记为 `false`,表示笔记已经创建或更新完成。 \ No newline at end of file diff --git a/src/net/micode/notes/gtask/data/Task.java b/src/net/micode/notes/gtask/data/Task.java index 6a19454..2b086c9 100644 --- a/src/net/micode/notes/gtask/data/Task.java +++ b/src/net/micode/notes/gtask/data/Task.java @@ -1,50 +1,26 @@ -/* - * 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.gtask.data; -import android.database.Cursor; -import android.text.TextUtils; -import android.util.Log; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - - +// Task类继承自Node类,用于表示一个任务对象 public class Task extends Node { + // 日志标签,用于标识日志信息来源 private static final String TAG = Task.class.getSimpleName(); + // 表示任务是否已完成 private boolean mCompleted; + // 任务的备注信息 private String mNotes; + // 任务的元信息,使用JSONObject存储 private JSONObject mMetaInfo; + // 任务的前一个兄弟任务,用于任务排序 private Task mPriorSibling; + // 任务所属的任务列表 private TaskList mParent; + // Task类的构造方法,初始化任务的默认状态 public Task() { super(); mCompleted = false; @@ -54,6 +30,12 @@ public class Task extends Node { mMetaInfo = null; } + /** + * 生成任务创建操作的JSON对象 + * @param actionId 操作ID + * @return 返回表示创建操作的JSONObject + * @throws ActionFailureException 如果JSON对象生成失败,则抛出此异常 + */ public JSONObject getCreateAction(int actionId) { JSONObject js = new JSONObject(); @@ -103,6 +85,12 @@ public class Task extends Node { return js; } + /** + * 生成任务更新操作的JSON对象 + * @param actionId 操作ID + * @return 返回表示更新操作的JSONObject + * @throws ActionFailureException 如果JSON对象生成失败,则抛出此异常 + */ public JSONObject getUpdateAction(int actionId) { JSONObject js = new JSONObject(); @@ -135,6 +123,11 @@ public class Task extends Node { return js; } + /** + * 通过远程JSON对象设置任务内容 + * @param js 远程JSON对象,包含任务的相关信息 + * @throws ActionFailureException 如果设置内容失败,则抛出此异常 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { @@ -175,177 +168,220 @@ public class Task extends Node { } } + /** + * 通过本地JSON对象设置任务内容 + * @param js 本地JSON对象,包含任务的相关信息 + * @throws ActionFailureException 如果设置内容失败,则抛出此异常 + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) || !js.has(GTaskStringUtils.META_HEAD_DATA)) { - Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); - } +``` - try { - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); +// 当没有可用内容时记录警告信息 +Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); +} + +try { + // 从JSON对象中获取笔记和数据数组 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { - Log.e(TAG, "invalid type"); - return; + // 检查笔记类型是否无效 + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + Log.e(TAG, "invalid type"); + return; + } + + // 遍历数据数组以查找并设置笔记内容 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + setName(data.getString(DataColumns.CONTENT)); + break; + } + } + +} catch (JSONException e) { + // 记录并打印JSONException的堆栈跟踪 + Log.e(TAG, e.toString()); + e.printStackTrace(); +} +``` +/** + * 从内容生成本地使用的JSON对象 + * + * @return 表示笔记内容的JSONObject,如果内容为空则返回null + */ +public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + // 处理从网页创建的新任务 + if (mMetaInfo == null) { + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; } + // 构建新的笔记JSON对象 + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + data.put(DataColumns.CONTENT, name); + dataArray.put(data); + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + // 处理已同步的任务 + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 更新数据数组中的笔记内容 for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - setName(data.getString(DataColumns.CONTENT)); + data.put(DataColumns.CONTENT, getName()); break; } } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; } + } catch (JSONException e) { + // 记录并打印JSONException的堆栈跟踪 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; } - - public JSONObject getLocalJSONFromContent() { - String name = getName(); +} +/** + * 设置任务的元信息 + * + * @param metaData 包含笔记信息的MetaData对象 + */ +public void setMetaInfo(MetaData metaData) { + if (metaData != null && metaData.getNotes() != null) { try { - if (mMetaInfo == null) { - // new task created from web - if (name == null) { - Log.w(TAG, "the note seems to be an empty one"); - return null; - } - - JSONObject js = new JSONObject(); - JSONObject note = new JSONObject(); - JSONArray dataArray = new JSONArray(); - JSONObject data = new JSONObject(); - data.put(DataColumns.CONTENT, name); - dataArray.put(data); - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - return js; - } else { - // synced task - JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - data.put(DataColumns.CONTENT, getName()); - break; - } - } - - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - return mMetaInfo; - } + // 将笔记信息转换为JSON对象 + mMetaInfo = new JSONObject(metaData.getNotes()); } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; + // 转换失败时记录错误并将mMetaInfo设置为null + Log.w(TAG, e.toString()); + mMetaInfo = null; } } +} +``` +```java +/** + * 根据当前状态和数据库游标确定同步操作 + * + * @param c 指向数据库中笔记数据的游标 + * @return 表示要执行的同步操作的整数 + */ +public int getSyncAction(Cursor c) { + try { + // 从mMetaInfo中获取笔记信息 + JSONObject noteInfo = null; + if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { + noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + } - public void setMetaInfo(MetaData metaData) { - if (metaData != null && metaData.getNotes() != null) { - try { - mMetaInfo = new JSONObject(metaData.getNotes()); - } catch (JSONException e) { - Log.w(TAG, e.toString()); - mMetaInfo = null; - } + // 根据笔记信息确定同步操作 + if (noteInfo == null) { + Log.w(TAG, "it seems that note meta has been deleted"); + return SYNC_ACTION_UPDATE_REMOTE; } - } - public int getSyncAction(Cursor c) { - try { - JSONObject noteInfo = null; - if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { - noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - } + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + return SYNC_ACTION_UPDATE_LOCAL; + } - if (noteInfo == null) { - Log.w(TAG, "it seems that note meta has been deleted"); - return SYNC_ACTION_UPDATE_REMOTE; - } + // 验证笔记ID + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); + return SYNC_ACTION_UPDATE_LOCAL; + } - if (!noteInfo.has(NoteColumns.ID)) { - Log.w(TAG, "remote note id seems to be deleted"); + // 根据本地修改状态确定同步操作 + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + return SYNC_ACTION_NONE; + } else { return SYNC_ACTION_UPDATE_LOCAL; } - - // validate the note id now - if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { - Log.w(TAG, "note id doesn't match"); - return SYNC_ACTION_UPDATE_LOCAL; + } else { + // 验证GTasks ID + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; } - - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side - return SYNC_ACTION_NONE; - } else { - // apply remote to local - return SYNC_ACTION_UPDATE_LOCAL; - } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + return SYNC_ACTION_UPDATE_REMOTE; } else { - // validate gtask id - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; - } - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only - return SYNC_ACTION_UPDATE_REMOTE; - } else { - return SYNC_ACTION_UPDATE_CONFLICT; - } + return SYNC_ACTION_UPDATE_CONFLICT; } - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); } - - return SYNC_ACTION_ERROR; - } - - public boolean isWorthSaving() { - return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) - || (getNotes() != null && getNotes().trim().length() > 0); + } catch (Exception e) { + // 记录并打印异常的堆栈跟踪 + Log.e(TAG, e.toString()); + e.printStackTrace(); } - public void setCompleted(boolean completed) { - this.mCompleted = completed; - } - - public void setNotes(String notes) { - this.mNotes = notes; - } + return SYNC_ACTION_ERROR; +} +``` +```java +/** + * 检查任务是否有值得保存的内容 + * + * @return 布尔值,表示任务是否有值得保存的内容 + */ +public boolean isWorthSaving() { + return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) + || (getNotes() != null && getNotes().trim().length() > 0); +} +// 设置任务的完成状态 +public void setCompleted(boolean completed) { + this.mCompleted = completed; +} - public void setPriorSibling(Task priorSibling) { - this.mPriorSibling = priorSibling; - } +// 设置任务的笔记内容 +public void setNotes(String notes) { + this.mNotes = notes; +} - public void setParent(TaskList parent) { - this.mParent = parent; - } +// 设置任务的前一个兄弟任务 +public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; +} - public boolean getCompleted() { - return this.mCompleted; - } +// 设置任务的父任务列表 +public void setParent(TaskList parent) { + this.mParent = parent; +} - public String getNotes() { - return this.mNotes; - } +// 获取任务的完成状态 +public boolean getCompleted() { + return this.mCompleted; +} - public Task getPriorSibling() { - return this.mPriorSibling; - } +// 获取任务的笔记内容 +public String getNotes() { + return this.mNotes; +} - public TaskList getParent() { - return this.mParent; - } +// 获取任务的前一个兄弟任务 +public Task getPriorSibling() { + return this.mPriorSibling; +} +// 获取任务的父任务列表 +public TaskList getParent() { + return this.mParent; } diff --git a/src/net/micode/notes/gtask/data/TaskList.java b/src/net/micode/notes/gtask/data/TaskList.java index 4ea21c5..8ed7a27 100644 --- a/src/net/micode/notes/gtask/data/TaskList.java +++ b/src/net/micode/notes/gtask/data/TaskList.java @@ -1,18 +1,3 @@ -/* - * 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.gtask.data; @@ -29,20 +14,35 @@ import org.json.JSONObject; import java.util.ArrayList; - +/** + * TaskList 类代表一个任务列表,继承自 Node 类 + * 它包含一个任务列表和相关的操作,如创建和更新任务列表的 JSON 对象 + */ public class TaskList extends Node { private static final String TAG = TaskList.class.getSimpleName(); + // 任务列表的索引 private int mIndex; + // 任务列表 private ArrayList mChildren; + /** + * TaskList 构造函数初始化任务列表和索引 + */ public TaskList() { super(); mChildren = new ArrayList(); mIndex = 1; } + /** + * 生成创建任务列表的 JSON 对象 + * + * @param actionId 操作的 ID + * @return 创建操作的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getCreateAction(int actionId) { JSONObject js = new JSONObject(); @@ -74,6 +74,13 @@ public class TaskList extends Node { return js; } + /** + * 生成更新任务列表的 JSON 对象 + * + * @param actionId 操作的 ID + * @return 更新操作的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getUpdateAction(int actionId) { JSONObject js = new JSONObject(); @@ -103,6 +110,12 @@ public class TaskList extends Node { return js; } + /** + * 通过远程 JSON 对象设置任务列表的内容 + * + * @param js 远程 JSON 对象 + * @throws ActionFailureException 如果设置内容失败 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { @@ -129,6 +142,12 @@ public class TaskList extends Node { } } + /** + * 通过本地 JSON 对象设置任务列表的内容 + * + * @param js 本地 JSON 对象 + * @throws ActionFailureException 如果设置内容失败 + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); @@ -153,191 +172,277 @@ public class TaskList extends Node { } } catch (JSONException e) { Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } + e.printStackTrace(); +} +} - public JSONObject getLocalJSONFromContent() { - try { - JSONObject js = new JSONObject(); - JSONObject folder = new JSONObject(); - - String folderName = getName(); - if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) - folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), - folderName.length()); - folder.put(NoteColumns.SNIPPET, folderName); - if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) - || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) - folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - else - folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); - - js.put(GTaskStringUtils.META_HEAD_NOTE, folder); - - return js; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; - } - } +/** + * 获取本地 JSON 内容 + * + * 该方法构建一个表示本地笔记信息的 JSONObject,包括文件夹名称和类型 + * @return 包含本地笔记信息的 JSONObject,如果发生异常则返回 null + */ +public JSONObject getLocalJSONFromContent() { + try { + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); - public int getSyncAction(Cursor c) { - try { - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side - return SYNC_ACTION_NONE; - } else { - // apply remote to local - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // validate gtask id - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; - } - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only - return SYNC_ACTION_UPDATE_REMOTE; - } else { - // for folder conflicts, just apply local modification - return SYNC_ACTION_UPDATE_REMOTE; - } - } - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } + String folderName = getName(); + if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) + folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), + folderName.length()); + folder.put(NoteColumns.SNIPPET, folderName); + if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) + || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) + folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + else + folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); - return SYNC_ACTION_ERROR; - } + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); - public int getChildTaskCount() { - return mChildren.size(); + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; } +} - public boolean addChildTask(Task task) { - boolean ret = false; - if (task != null && !mChildren.contains(task)) { - ret = mChildren.add(task); - if (ret) { - // need to set prior sibling and parent - task.setPriorSibling(mChildren.isEmpty() ? null : mChildren - .get(mChildren.size() - 1)); - task.setParent(this); +/** + * 根据游标获取同步操作 + * + * 根据游标信息确定本地和远程笔记之间的同步操作 + * @param c 指向数据库记录的游标 + * @return 同步操作常量之一 + */ +public int getSyncAction(Cursor c) { + try { + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // 本地没有更新 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 双方都没有更新 + return SYNC_ACTION_NONE; + } else { + // 将远程应用到本地 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // 验证 gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id 不匹配"); + return SYNC_ACTION_ERROR; + } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 仅本地有修改 + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // 对于文件夹冲突,仅应用本地修改 + return SYNC_ACTION_UPDATE_REMOTE; } } - return ret; + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); } - public boolean addChildTask(Task task, int index) { - if (index < 0 || index > mChildren.size()) { - Log.e(TAG, "add child task: invalid index"); - return false; - } + return SYNC_ACTION_ERROR; +} - int pos = mChildren.indexOf(task); - if (task != null && pos == -1) { - mChildren.add(index, task); - - // update the task list - Task preTask = null; - Task afterTask = null; - if (index != 0) - preTask = mChildren.get(index - 1); - if (index != mChildren.size() - 1) - afterTask = mChildren.get(index + 1); - - task.setPriorSibling(preTask); - if (afterTask != null) - afterTask.setPriorSibling(task); - } +/** + * 获取子任务的数量 + * + * @return 子任务的数量 + */ +public int getChildTaskCount() { + return mChildren.size(); +} - return true; +/** + * 添加子任务 + * + * 如果子任务列表中不存在该任务,则将其添加到子任务列表中 + * @param task 要添加的任务 + * @return 如果任务成功添加则返回 true,否则返回 false + */ +public boolean addChildTask(Task task) { + boolean ret = false; + if (task != null && !mChildren.contains(task)) { + ret = mChildren.add(task); + if (ret) { + // 需要设置前一个兄弟任务和父任务 + task.setPriorSibling(mChildren.isEmpty() ? null : mChildren + .get(mChildren.size() - 1)); + task.setParent(this); + } } + return ret; +} - public boolean removeChildTask(Task task) { - boolean ret = false; - int index = mChildren.indexOf(task); - if (index != -1) { - ret = mChildren.remove(task); - - if (ret) { - // reset prior sibling and parent - task.setPriorSibling(null); - task.setParent(null); - - // update the task list - if (index != mChildren.size()) { - mChildren.get(index).setPriorSibling( - index == 0 ? null : mChildren.get(index - 1)); - } - } - } - return ret; +/** + * 在指定索引位置添加子任务 + * + * 将任务添加到子任务列表的指定索引位置,并更新任务列表 + * @param task 要添加的任务 + * @param index 要添加任务的索引位置 + * @return 如果任务成功添加则返回 true,否则返回 false + */ +public boolean addChildTask(Task task, int index) { + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "添加子任务: 无效的索引"); + return false; } - public boolean moveChildTask(Task task, int index) { + int pos = mChildren.indexOf(task); + if (task != null && pos == -1) { + mChildren.add(index, task); + + // 更新任务列表 + Task preTask = null; + Task afterTask = null; + if (index != 0) + preTask = mChildren.get(index - 1); + if (index != mChildren.size() - 1) + afterTask = mChildren.get(index + 1); + + task.setPriorSibling(preTask); + if (afterTask != null) + afterTask.setPriorSibling(task); + } - if (index < 0 || index >= mChildren.size()) { - Log.e(TAG, "move child task: invalid index"); - return false; - } + return true; +} - int pos = mChildren.indexOf(task); - if (pos == -1) { - Log.e(TAG, "move child task: the task should in the list"); - return false; +/** + * 移除子任务 + * + * 从子任务列表中移除任务,并更新任务列表 + * @param task 要移除的任务 + * @return 如果任务成功移除则返回 true,否则返回 false + */ +public boolean removeChildTask(Task task) { + boolean ret = false; + int index = mChildren.indexOf(task); + if (index != -1) { + ret = mChildren.remove(task); + + if (ret) { + // 重置前一个兄弟任务和父任务 + task.setPriorSibling(null); + task.setParent(null); + + // 更新任务列表 + if (index != mChildren.size()) { + mChildren.get(index).setPriorSibling( + index == 0 ? null : mChildren.get(index - 1)); + } } - - if (pos == index) - return true; - return (removeChildTask(task) && addChildTask(task, index)); } + return ret; +} - public Task findChildTaskByGid(String gid) { - for (int i = 0; i < mChildren.size(); i++) { - Task t = mChildren.get(i); - if (t.getGid().equals(gid)) { - return t; - } - } - return null; +/** + * 移动子任务到新的索引位置 + * + * 将任务在子任务列表中移动到新的索引位置 + * @param task 要移动的任务 + * @param index 新的索引位置 + * @return 如果任务成功移动则返回 true,否则返回 false + */ +public boolean moveChildTask(Task task, int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "移动子任务: 无效的索引"); + return false; } - public int getChildTaskIndex(Task task) { - return mChildren.indexOf(task); + int pos = mChildren.indexOf(task); + if (pos == -1) { + Log.e(TAG, "移动子任务: 任务不在列表中"); + return false; } - public Task getChildTaskByIndex(int index) { - if (index < 0 || index >= mChildren.size()) { - Log.e(TAG, "getTaskByIndex: invalid index"); - return null; + if (pos == index) + return true; + return (removeChildTask(task) && addChildTask(task, index)); +} + +/** + * 通过 GID 查找子任务 + * + * @param gid 要查找的任务的 GID + * @return 找到的任务,如果没有找到则返回 null + */ +public Task findChildTaskByGid(String gid) { + for (int i = 0; i < mChildren.size(); i++) { + Task t = mChildren.get(i); + if (t.getGid().equals(gid)) { + return t; } - return mChildren.get(index); } + return null; +} - public Task getChilTaskByGid(String gid) { - for (Task task : mChildren) { - if (task.getGid().equals(gid)) - return task; - } +/** + * 获取子任务的索引 + * + * @param task 子任务 + * @return 子任务的索引,如果没有找到则返回 -1 + */ +public int getChildTaskIndex(Task task) { + return mChildren.indexOf(task); +} + +/** + * 通过索引获取子任务 + * + * @param index 子任务的索引 + * @return 指定索引位置的子任务,如果索引无效则返回 null + */ +public Task getChildTaskByIndex(int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "通过索引获取任务: 无效的索引"); return null; } + return mChildren.get(index); +} - public ArrayList getChildTaskList() { - return this.mChildren; +/** + * 通过 GID 查找子任务(替代方法) + * + * @param gid 要查找的任务的 GID + * @return 找到的任务,如果没有找到则返回 null + */ +public Task getChilTaskByGid(String gid) { + for (Task task : mChildren) { + if (task.getGid().equals(gid)) + return task; } + return null; +} - public void setIndex(int index) { - this.mIndex = index; - } +/** + * 获取子任务列表 + * + * @return 子任务列表 + */ +public ArrayList getChildTaskList() { + return this.mChildren; +} - public int getIndex() { - return this.mIndex; - } +/** + * 设置任务的索引 + * + * @param index 任务的新索引 + */ +public void setIndex(int index) { + this.mIndex = index; +} + +/** + * 获取任务的索引 + * + * @return 任务的索引 + */ +public int getIndex() { + return this.mIndex; +} } diff --git a/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/net/micode/notes/gtask/exception/ActionFailureException.java index 15504be..50a6e6d 100644 --- a/src/net/micode/notes/gtask/exception/ActionFailureException.java +++ b/src/net/micode/notes/gtask/exception/ActionFailureException.java @@ -1,33 +1,41 @@ -/* - * 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.gtask.exception; +/** + * ActionFailureException 类用于表示操作失败时抛出的异常。 + * 继承自 RuntimeException,表示该异常是运行时异常,不需要强制捕获。 + */ public class ActionFailureException extends RuntimeException { + /** + * 序列化版本UID,用于确保序列化和反序列化的一致性。 + */ private static final long serialVersionUID = 4425249765923293627L; + /** + * 默认构造函数,无参数。 + */ public ActionFailureException() { super(); } - public ActionFailureException(String paramString) { - super(paramString); + /** + * 带有错误消息的构造函数。 + * + * @param message 错误消息 + */ + public ActionFailureException(String message) { + super(message); } - public ActionFailureException(String paramString, Throwable paramThrowable) { - super(paramString, paramThrowable); + /** + * 带有错误消息和原因的构造函数。 + * + * @param message 错误消息 + * @param cause 引发此异常的原因(可选) + */ + public ActionFailureException(String message, Throwable cause) { + super(message, cause); } } + diff --git a/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/net/micode/notes/gtask/exception/NetworkFailureException.java index b08cfb1..081f2aa 100644 --- a/src/net/micode/notes/gtask/exception/NetworkFailureException.java +++ b/src/net/micode/notes/gtask/exception/NetworkFailureException.java @@ -1,33 +1,41 @@ -/* - * 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.gtask.exception; +/** + * NetworkFailureException 类用于表示网络操作失败时抛出的异常。 + * 继承自 Exception,表示该异常是检查型异常,需要在代码中显式捕获或声明抛出。 + */ public class NetworkFailureException extends Exception { + /** + * 序列化版本UID,用于确保序列化和反序列化的一致性。 + */ private static final long serialVersionUID = 2107610287180234136L; + /** + * 默认构造函数,无参数。 + */ public NetworkFailureException() { super(); } - public NetworkFailureException(String paramString) { - super(paramString); + /** + * 带有错误消息的构造函数。 + * + * @param message 错误消息 + */ + public NetworkFailureException(String message) { + super(message); } - public NetworkFailureException(String paramString, Throwable paramThrowable) { - super(paramString, paramThrowable); + /** + * 带有错误消息和原因的构造函数。 + * + * @param message 错误消息 + * @param cause 引发此异常的原因(可选) + */ + public NetworkFailureException(String message, Throwable cause) { + super(message, cause); } } + diff --git a/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java index b3b61e7..ca153e0 100644 --- a/src/net/micode/notes/gtask/remote/GTaskASyncTask.java +++ b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java @@ -1,20 +1,3 @@ - -/* - * 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.gtask.remote; import android.app.Notification; @@ -25,70 +8,117 @@ import android.content.Intent; import android.os.AsyncTask; import net.micode.notes.R; +import net.micode.notes.gtask.remote.GTaskManager; +import net.micode.notes.gtask.remote.GTaskSyncService; import net.micode.notes.ui.NotesListActivity; import net.micode.notes.ui.NotesPreferenceActivity; - +/** + * GTaskASyncTask 类用于异步执行 Google Task 同步任务。 + * 它继承自 AsyncTask,并在后台线程中执行同步操作,同时通过通知和回调接口向用户反馈进度和结果。 + */ public class GTaskASyncTask extends AsyncTask { + /** + * 同步任务的通知 ID。 + */ private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + /** + * 定义一个完成监听器接口,用于在任务完成后调用。 + */ public interface OnCompleteListener { void onComplete(); } + /** + * 上下文对象,用于访问应用程序资源。 + */ private Context mContext; + /** + * 通知管理器,用于显示同步任务的通知。 + */ private NotificationManager mNotifiManager; + /** + * 任务管理器,用于执行实际的同步逻辑。 + */ private GTaskManager mTaskManager; + /** + * 完成监听器实例,用于在任务完成后触发回调。 + */ private OnCompleteListener mOnCompleteListener; + /** + * 构造函数,初始化上下文和监听器。 + * + * @param context 应用程序上下文 + * @param listener 完成监听器 + */ public GTaskASyncTask(Context context, OnCompleteListener listener) { mContext = context; mOnCompleteListener = listener; - mNotifiManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); + mNotifiManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); mTaskManager = GTaskManager.getInstance(); } + /** + * 取消同步任务。 + */ public void cancelSync() { mTaskManager.cancelSync(); } + /** + * 发布进度消息。 + * + * @param message 进度消息 + */ public void publishProgess(String message) { - publishProgress(new String[] { - message - }); + publishProgress(new String[]{message}); } + /** + * 显示通知。 + * + * @param tickerId 提示文本资源ID + * @param content 通知内容 + */ private void showNotification(int tickerId, String content) { - Notification notification = new Notification(R.drawable.notification, mContext - .getString(tickerId), System.currentTimeMillis()); + Notification notification = new Notification(R.drawable.notification, mContext.getString(tickerId), System.currentTimeMillis()); notification.defaults = Notification.DEFAULT_LIGHTS; notification.flags = Notification.FLAG_AUTO_CANCEL; PendingIntent pendingIntent; if (tickerId != R.string.ticker_success) { - pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesPreferenceActivity.class), 0); - + // 如果同步失败,跳转到设置页面 + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, NotesPreferenceActivity.class), 0); } else { - pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesListActivity.class), 0); + // 如果同步成功,跳转到笔记列表页面 + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, NotesListActivity.class), 0); } - notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, - pendingIntent); + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, pendingIntent); mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); } + /** + * 在后台线程中执行同步任务。 + * + * @param unused 未使用的参数 + * @return 同步结果状态码 + */ @Override protected Integer doInBackground(Void... unused) { - publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity - .getSyncAccountName(mContext))); + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity.getSyncAccountName(mContext))); return mTaskManager.sync(mContext, this); } + /** + * 更新进度时调用此方法。 + * + * @param progress 进度消息数组 + */ @Override protected void onProgressUpdate(String... progress) { showNotification(R.string.ticker_syncing, progress[0]); @@ -97,23 +127,25 @@ public class GTaskASyncTask extends AsyncTask { } } + /** + * 任务完成后调用此方法。 + * + * @param result 同步结果状态码 + */ @Override protected void onPostExecute(Integer result) { if (result == GTaskManager.STATE_SUCCESS) { - showNotification(R.string.ticker_success, mContext.getString( - R.string.success_sync_account, mTaskManager.getSyncAccount())); + showNotification(R.string.ticker_success, mContext.getString(R.string.success_sync_account, mTaskManager.getSyncAccount())); NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); } else if (result == GTaskManager.STATE_NETWORK_ERROR) { showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network)); } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal)); } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { - showNotification(R.string.ticker_cancel, mContext - .getString(R.string.error_sync_cancelled)); + showNotification(R.string.ticker_cancel, mContext.getString(R.string.error_sync_cancelled)); } if (mOnCompleteListener != null) { new Thread(new Runnable() { - public void run() { mOnCompleteListener.onComplete(); } diff --git a/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/net/micode/notes/gtask/remote/GTaskClient.java index c67dfdf..c5439dd 100644 --- a/src/net/micode/notes/gtask/remote/GTaskClient.java +++ b/src/net/micode/notes/gtask/remote/GTaskClient.java @@ -1,20 +1,21 @@ -/* - * 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.gtask.remote; + /* + * 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.gtask.remote; import android.accounts.Account; import android.accounts.AccountManager; @@ -44,7 +45,6 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.json.JSONArray; import org.json.JSONException; @@ -60,36 +60,34 @@ import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; - +/** + * GTaskClient 类用于处理与 Google Tasks 的远程交互,包括登录、创建任务、移动任务等操作。 + */ public class GTaskClient { private static final String TAG = GTaskClient.class.getSimpleName(); + // Google Tasks API URL private static final String GTASK_URL = "https://mail.google.com/tasks/"; - private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; - private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + // 单例模式实例 private static GTaskClient mInstance = null; + // HTTP 客户端和相关配置 private DefaultHttpClient mHttpClient; - private String mGetUrl; - private String mPostUrl; - private long mClientVersion; - private boolean mLoggedin; - private long mLastLoginTime; - private int mActionId; - private Account mAccount; - private JSONArray mUpdateArray; + /** + * 构造函数,初始化默认值。 + */ private GTaskClient() { mHttpClient = null; mGetUrl = GTASK_GET_URL; @@ -102,6 +100,11 @@ public class GTaskClient { mUpdateArray = null; } + /** + * 获取单例实例。 + * + * @return GTaskClient 实例 + */ public static synchronized GTaskClient getInstance() { if (mInstance == null) { mInstance = new GTaskClient(); @@ -109,36 +112,38 @@ public class GTaskClient { return mInstance; } + /** + * 登录 Google 账户并获取授权令牌。 + * + * @param activity 当前活动的上下文 + * @return 是否登录成功 + */ public boolean login(Activity activity) { - // we suppose that the cookie would expire after 5 minutes - // then we need to re-login + // 如果上次登录时间超过5分钟,则需要重新登录 final long interval = 1000 * 60 * 5; if (mLastLoginTime + interval < System.currentTimeMillis()) { mLoggedin = false; } - // need to re-login after account switch - if (mLoggedin - && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity - .getSyncAccountName(activity))) { + // 如果切换了账户,则需要重新登录 + if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity.getSyncAccountName(activity))) { mLoggedin = false; } if (mLoggedin) { - Log.d(TAG, "already logged in"); + Log.d(TAG, "已经登录"); return true; } mLastLoginTime = System.currentTimeMillis(); String authToken = loginGoogleAccount(activity, false); if (authToken == null) { - Log.e(TAG, "login google account failed"); + Log.e(TAG, "登录 Google 账户失败"); return false; } - // login with custom domain if necessary - if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() - .endsWith("googlemail.com"))) { + // 如果是自定义域名,则使用特定的 URL 进行登录 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase().endsWith("googlemail.com"))) { StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); int index = mAccount.name.indexOf('@') + 1; String suffix = mAccount.name.substring(index); @@ -151,7 +156,7 @@ public class GTaskClient { } } - // try to login with google official url + // 尝试使用官方 URL 登录 if (!mLoggedin) { mGetUrl = GTASK_GET_URL; mPostUrl = GTASK_POST_URL; @@ -164,13 +169,20 @@ public class GTaskClient { return true; } + /** + * 获取 Google 账户的授权令牌。 + * + * @param activity 当前活动的上下文 + * @param invalidateToken 是否无效化当前令牌 + * @return 授权令牌 + */ private String loginGoogleAccount(Activity activity, boolean invalidateToken) { String authToken; AccountManager accountManager = AccountManager.get(activity); Account[] accounts = accountManager.getAccountsByType("com.google"); if (accounts.length == 0) { - Log.e(TAG, "there is no available google account"); + Log.e(TAG, "没有可用的 Google 账户"); return null; } @@ -185,13 +197,12 @@ public class GTaskClient { if (account != null) { mAccount = account; } else { - Log.e(TAG, "unable to get an account with the same name in the settings"); + Log.e(TAG, "无法找到与设置中相同的账户"); return null; } - // get the token now - AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, - "goanna_mobile", null, activity, null, null); + // 获取授权令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, "goanna_mobile", null, activity, null, null); try { Bundle authTokenBundle = accountManagerFuture.getResult(); authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); @@ -200,31 +211,43 @@ public class GTaskClient { loginGoogleAccount(activity, false); } } catch (Exception e) { - Log.e(TAG, "get auth token failed"); + Log.e(TAG, "获取授权令牌失败"); authToken = null; } return authToken; } + /** + * 尝试使用授权令牌登录 Google Tasks。 + * + * @param activity 当前活动的上下文 + * @param authToken 授权令牌 + * @return 是否登录成功 + */ private boolean tryToLoginGtask(Activity activity, String authToken) { if (!loginGtask(authToken)) { - // maybe the auth token is out of date, now let's invalidate the - // token and try again + // 如果授权令牌过期,则尝试重新获取并再次登录 authToken = loginGoogleAccount(activity, true); if (authToken == null) { - Log.e(TAG, "login google account failed"); + Log.e(TAG, "登录 Google 账户失败"); return false; } if (!loginGtask(authToken)) { - Log.e(TAG, "login gtask failed"); + Log.e(TAG, "登录 Google Tasks 失败"); return false; } } return true; } + /** + * 使用授权令牌登录 Google Tasks。 + * + * @param authToken 授权令牌 + * @return 是否登录成功 + */ private boolean loginGtask(String authToken) { int timeoutConnection = 10000; int timeoutSocket = 15000; @@ -236,14 +259,14 @@ public class GTaskClient { mHttpClient.setCookieStore(localBasicCookieStore); HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); - // login gtask + // 登录 Google Tasks try { String loginUrl = mGetUrl + "?auth=" + authToken; HttpGet httpGet = new HttpGet(loginUrl); HttpResponse response = null; response = mHttpClient.execute(httpGet); - // get the cookie now + // 获取 Cookie List cookies = mHttpClient.getCookieStore().getCookies(); boolean hasAuthCookie = false; for (Cookie cookie : cookies) { @@ -252,10 +275,10 @@ public class GTaskClient { } } if (!hasAuthCookie) { - Log.w(TAG, "it seems that there is no auth cookie"); + Log.w(TAG, "似乎没有授权 Cookie"); } - // get the client version + // 获取客户端版本 String resString = getResponseContent(response.getEntity()); String jsBegin = "_setup("; String jsEnd = ")}"; @@ -272,18 +295,27 @@ public class GTaskClient { e.printStackTrace(); return false; } catch (Exception e) { - // simply catch all exceptions - Log.e(TAG, "httpget gtask_url failed"); + Log.e(TAG, "HTTP GET 请求失败"); return false; } return true; } + /** + * 获取下一个操作 ID。 + * + * @return 操作 ID + */ private int getActionId() { return mActionId++; } + /** + * 创建 HTTP POST 请求。 + * + * @return HttpPost 对象 + */ private HttpPost createHttpPost() { HttpPost httpPost = new HttpPost(mPostUrl); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); @@ -291,11 +323,18 @@ public class GTaskClient { return httpPost; } + /** + * 获取 HTTP 响应内容。 + * + * @param entity HTTP 响应实体 + * @return 响应内容字符串 + * @throws IOException IO 异常 + */ private String getResponseContent(HttpEntity entity) throws IOException { String contentEncoding = null; if (entity.getContentEncoding() != null) { contentEncoding = entity.getContentEncoding().getValue(); - Log.d(TAG, "encoding: " + contentEncoding); + Log.d(TAG, "编码: " + contentEncoding); } InputStream input = entity.getContent(); @@ -323,10 +362,17 @@ public class GTaskClient { } } + /** + * 发送 POST 请求并返回响应 JSON 对象。 + * + * @param js 请求 JSON 对象 + * @return 响应 JSON 对象 + * @throws NetworkFailureException 网络异常 + */ private JSONObject postRequest(JSONObject js) throws NetworkFailureException { if (!mLoggedin) { - Log.e(TAG, "please login first"); - throw new ActionFailureException("not logged in"); + Log.e(TAG, "请先登录"); + throw new ActionFailureException("未登录"); } HttpPost httpPost = createHttpPost(); @@ -336,7 +382,7 @@ public class GTaskClient { UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); httpPost.setEntity(entity); - // execute the post + // 执行 POST 请求 HttpResponse response = mHttpClient.execute(httpPost); String jsString = getResponseContent(response.getEntity()); return new JSONObject(jsString); @@ -344,83 +390,98 @@ public class GTaskClient { } catch (ClientProtocolException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new NetworkFailureException("postRequest failed"); + throw new NetworkFailureException("POST 请求失败"); } catch (IOException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new NetworkFailureException("postRequest failed"); + throw new NetworkFailureException("POST 请求失败"); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new ActionFailureException("unable to convert response content to jsonobject"); + throw new ActionFailureException("无法将响应内容转换为 JSON 对象"); } catch (Exception e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new ActionFailureException("error occurs when posting request"); + throw new ActionFailureException("发送请求时发生错误"); } } + /** + * 创建任务。 + * + * @param task 任务对象 + * @throws NetworkFailureException 网络异常 + */ public void createTask(Task task) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); - // action_list + // 添加创建任务的操作 actionList.put(task.getCreateAction(getActionId())); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client_version + // 设置客户端版本 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // post + // 发送请求 JSONObject jsResponse = postRequest(jsPost); - JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( - GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_RESULTS).get(0); task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new ActionFailureException("create task: handing jsonobject failed"); + throw new ActionFailureException("创建任务时处理 JSON 对象失败"); } } + /** + * 创建任务列表。 + * + * @param tasklist 任务列表对象 + * @throws NetworkFailureException 网络异常 + */ public void createTaskList(TaskList tasklist) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); - // action_list + // 添加创建任务列表的操作 actionList.put(tasklist.getCreateAction(getActionId())); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client version + // 设置客户端版本 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // post + // 发送请求 JSONObject jsResponse = postRequest(jsPost); - JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( - GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_RESULTS).get(0); tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new ActionFailureException("create tasklist: handing jsonobject failed"); + throw new ActionFailureException("创建任务列表时处理 JSON 对象失败"); } } + /** + * 提交更新操作。 + * + * @throws NetworkFailureException 网络异常 + */ public void commitUpdate() throws NetworkFailureException { if (mUpdateArray != null) { try { JSONObject jsPost = new JSONObject(); - // action_list + // 添加更新操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); - // client_version + // 设置客户端版本 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); postRequest(jsPost); @@ -428,15 +489,20 @@ public class GTaskClient { } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new ActionFailureException("commit update: handing jsonobject failed"); + throw new ActionFailureException("提交更新时处理 JSON 对象失败"); } } } + /** + * 添加更新节点。 + * + * @param node 节点对象 + * @throws NetworkFailureException 网络异常 + */ public void addUpdateNode(Node node) throws NetworkFailureException { if (node != null) { - // too many update items may result in an error - // set max to 10 items + // 更新项过多可能导致错误,限制最多 10 个 if (mUpdateArray != null && mUpdateArray.length() > 10) { commitUpdate(); } @@ -447,139 +513,87 @@ public class GTaskClient { } } - public void moveTask(Task task, TaskList preParent, TaskList curParent) - throws NetworkFailureException { + /** + * 移动任务。 + * + * @param task 任务对象 + * @param preParent 原父任务列表 + * @param curParent 新父任务列表 + * @throws NetworkFailureException 网络异常 + */ + public void moveTask(Task task, TaskList preParent, TaskList curParent) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); JSONObject action = new JSONObject(); - // action_list - action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + // 添加移动任务的操作 + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); if (preParent == curParent && task.getPriorSibling() != null) { - // put prioring_sibing_id only if moving within the tasklist and - // it is not the first one + // 如果在同一个任务列表内移动且不是第一个任务,则添加前一个兄弟任务 ID action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); } action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); if (preParent != curParent) { - // put the dest_list only if moving between tasklists + // 如果在不同任务列表之间移动,则添加目标任务列表 ID action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); } actionList.put(action); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - postRequest(jsPost); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("move task: handing jsonobject failed"); - } - } + 以下是为 `GTaskClient.java` 文件中选定代码段生成的中文注释: - public void deleteNode(Node node) throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - - // action_list - node.setDeleted(true); - actionList.put(node.getUpdateAction(getActionId())); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - postRequest(jsPost); - mUpdateArray = null; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("delete node: handing jsonobject failed"); - } - } - - public JSONArray getTaskLists() throws NetworkFailureException { - if (!mLoggedin) { - Log.e(TAG, "please login first"); - throw new ActionFailureException("not logged in"); - } - - try { - HttpGet httpGet = new HttpGet(mGetUrl); - HttpResponse response = null; - response = mHttpClient.execute(httpGet); - - // get the task list - String resString = getResponseContent(response.getEntity()); - String jsBegin = "_setup("; - String jsEnd = ")}"; - int begin = resString.indexOf(jsBegin); - int end = resString.lastIndexOf(jsEnd); - String jsString = null; - if (begin != -1 && end != -1 && begin < end) { - jsString = resString.substring(begin + jsBegin.length(), end); +```java +/** + * 获取指定任务列表中的所有任务。 + * + * @param listGid 任务列表的全局唯一标识符(GID) + * @return 包含任务的 JSONArray + * @throws NetworkFailureException 网络异常 + */ + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + commitUpdate(); // 提交任何未完成的更新操作 + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject(); + + // 添加获取所有任务的操作到操作列表 + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); // 获取唯一的操作 ID + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); // 设置任务列表 ID + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); // 不获取已删除的任务 + actionList.put(action); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // 将操作列表添加到请求 JSON 对象 + + // 设置客户端版本 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // 发送 POST 请求并获取响应 + JSONObject jsResponse = postRequest(jsPost); + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); // 返回任务列表的 JSONArray + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("获取任务列表时处理 JSON 对象失败"); + } } - JSONObject js = new JSONObject(jsString); - return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); - } catch (ClientProtocolException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("gettasklists: httpget failed"); - } catch (IOException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("gettasklists: httpget failed"); - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("get task lists: handing jasonobject failed"); - } - } - public JSONArray getTaskList(String listGid) throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - JSONObject action = new JSONObject(); - - // action_list - action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); - action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); - action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); - action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); - actionList.put(action); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - JSONObject jsResponse = postRequest(jsPost); - return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("get task list: handing jsonobject failed"); - } - } - - public Account getSyncAccount() { - return mAccount; - } +/** + * 获取当前同步的 Google 账户。 + * + * @return 当前同步的 Account 对象 + */ + public Account getSyncAccount() { + return mAccount; + } - public void resetUpdateArray() { - mUpdateArray = null; - } -} +/** + * 重置更新数组,清除所有待提交的更新操作。 + */ + public void resetUpdateArray() { + mUpdateArray = null; + } diff --git a/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/net/micode/notes/gtask/remote/GTaskManager.java index d2b4082..8067561 100644 --- a/src/net/micode/notes/gtask/remote/GTaskManager.java +++ b/src/net/micode/notes/gtask/remote/GTaskManager.java @@ -1,20 +1,18 @@ -/* - * 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.gtask.remote; + /* + * 版权所有 (c) 2010-2011, MiCode 开源社区 (www.micode.net) + * + * 根据 Apache License 2.0 许可证授权; + * 除非符合许可证规定,否则不得使用此文件。 + * 您可以从以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,否则根据许可证分发的软件按“原样”分发, + * 不提供任何明示或暗示的保证或条件。请参阅许可证以了解具体的许可和限制。 + */ + + package net.micode.notes.gtask.remote; import android.app.Activity; import android.content.ContentResolver; @@ -30,11 +28,11 @@ import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.data.MetaData; import net.micode.notes.gtask.data.Node; -import net.micode.notes.gtask.data.SqlNote; import net.micode.notes.gtask.data.Task; import net.micode.notes.gtask.data.TaskList; import net.micode.notes.gtask.exception.ActionFailureException; import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.gtask.remote.GTaskASyncTask; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.GTaskStringUtils; @@ -47,58 +45,60 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Map; - +/** + * GTaskManager 类负责管理 Google Tasks 的同步操作。 + */ public class GTaskManager { private static final String TAG = GTaskManager.class.getSimpleName(); - public static final int STATE_SUCCESS = 0; - - public static final int STATE_NETWORK_ERROR = 1; - - public static final int STATE_INTERNAL_ERROR = 2; - - public static final int STATE_SYNC_IN_PROGRESS = 3; - - public static final int STATE_SYNC_CANCELLED = 4; + // 同步状态常量 + public static final int STATE_SUCCESS = 0; // 成功 + public static final int STATE_NETWORK_ERROR = 1; // 网络错误 + public static final int STATE_INTERNAL_ERROR = 2; // 内部错误 + public static final int STATE_SYNC_IN_PROGRESS = 3; // 同步正在进行中 + public static final int STATE_SYNC_CANCELLED = 4; // 同步被取消 + // 单例模式实例 private static GTaskManager mInstance = null; + // 上下文和活动对象 private Activity mActivity; - private Context mContext; - private ContentResolver mContentResolver; + // 同步标志 private boolean mSyncing; - private boolean mCancelled; + // 数据结构 private HashMap mGTaskListHashMap; - private HashMap mGTaskHashMap; - private HashMap mMetaHashMap; - private TaskList mMetaList; - private HashSet mLocalDeleteIdMap; - private HashMap mGidToNid; - private HashMap mNidToGid; + /** + * 构造函数,初始化数据结构。 + */ private GTaskManager() { mSyncing = false; mCancelled = false; - mGTaskListHashMap = new HashMap(); - mGTaskHashMap = new HashMap(); - mMetaHashMap = new HashMap(); + mGTaskListHashMap = new HashMap<>(); + mGTaskHashMap = new HashMap<>(); + mMetaHashMap = new HashMap<>(); mMetaList = null; - mLocalDeleteIdMap = new HashSet(); - mGidToNid = new HashMap(); - mNidToGid = new HashMap(); + mLocalDeleteIdMap = new HashSet<>(); + mGidToNid = new HashMap<>(); + mNidToGid = new HashMap<>(); } + /** + * 获取 GTaskManager 的单例实例。 + * + * @return GTaskManager 实例 + */ public static synchronized GTaskManager getInstance() { if (mInstance == null) { mInstance = new GTaskManager(); @@ -106,44 +106,47 @@ public class GTaskManager { return mInstance; } + /** + * 设置 Activity 上下文。 + * + * @param activity Activity 对象 + */ public synchronized void setActivityContext(Activity activity) { - // used for getting authtoken mActivity = activity; } + /** + * 执行同步操作。 + * + * @param context 上下文 + * @param asyncTask 异步任务 + * @return 同步状态码 + */ public int sync(Context context, GTaskASyncTask asyncTask) { if (mSyncing) { - Log.d(TAG, "Sync is in progress"); + Log.d(TAG, "同步正在进行中"); return STATE_SYNC_IN_PROGRESS; } mContext = context; mContentResolver = mContext.getContentResolver(); mSyncing = true; mCancelled = false; - mGTaskListHashMap.clear(); - mGTaskHashMap.clear(); - mMetaHashMap.clear(); - mLocalDeleteIdMap.clear(); - mGidToNid.clear(); - mNidToGid.clear(); try { GTaskClient client = GTaskClient.getInstance(); client.resetUpdateArray(); - // login google task - if (!mCancelled) { - if (!client.login(mActivity)) { - throw new NetworkFailureException("login google task failed"); - } + // 登录 Google Tasks + if (!mCancelled && !client.login(mActivity)) { + throw new NetworkFailureException("登录 Google Tasks 失败"); } - // get the task list from google - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + // 初始化任务列表 + asyncTask.publishProgress(mContext.getString(R.string.sync_progress_init_list)); initGTaskList(); - // do content sync work - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + // 执行内容同步 + asyncTask.publishProgress(mContext.getString(R.string.sync_progress_syncing)); syncContent(); } catch (NetworkFailureException e) { Log.e(TAG, e.toString()); @@ -168,29 +171,32 @@ public class GTaskManager { return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; } + /** + * 初始化任务列表。 + * + * @throws NetworkFailureException 网络失败异常 + */ private void initGTaskList() throws NetworkFailureException { - if (mCancelled) - return; + if (mCancelled) return; GTaskClient client = GTaskClient.getInstance(); try { JSONArray jsTaskLists = client.getTaskLists(); - // init meta list first + // 初始化元数据列表 mMetaList = null; for (int i = 0; i < jsTaskLists.length(); i++) { JSONObject object = jsTaskLists.getJSONObject(i); String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); - if (name - .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + if (name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { mMetaList = new TaskList(); mMetaList.setContentByRemoteJSON(object); - // load meta data + // 加载元数据 JSONArray jsMetas = client.getTaskList(gid); for (int j = 0; j < jsMetas.length(); j++) { - object = (JSONObject) jsMetas.getJSONObject(j); + object = jsMetas.getJSONObject(j); MetaData metaData = new MetaData(); metaData.setContentByRemoteJSON(object); if (metaData.isWorthSaving()) { @@ -203,32 +209,30 @@ public class GTaskManager { } } - // create meta list if not existed + // 如果元数据列表不存在,则创建 if (mMetaList == null) { mMetaList = new TaskList(); - mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META); GTaskClient.getInstance().createTaskList(mMetaList); } - // init task list + // 初始化任务列表 for (int i = 0; i < jsTaskLists.length(); i++) { JSONObject object = jsTaskLists.getJSONObject(i); String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); - if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) - && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META)) { + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) && + !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { TaskList tasklist = new TaskList(); tasklist.setContentByRemoteJSON(object); mGTaskListHashMap.put(gid, tasklist); mGTaskHashMap.put(gid, tasklist); - // load tasks + // 加载任务 JSONArray jsTasks = client.getTaskList(gid); for (int j = 0; j < jsTasks.length(); j++) { - object = (JSONObject) jsTasks.getJSONObject(j); + object = jsTasks.getJSONObject(j); gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); Task task = new Task(); task.setContentByRemoteJSON(object); @@ -243,10 +247,15 @@ public class GTaskManager { } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); - throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + throw new ActionFailureException("处理 JSONObject 失败"); } } + /** + * 执行内容同步。 + * + * @throws NetworkFailureException 网络失败异常 + */ private void syncContent() throws NetworkFailureException { int syncType; Cursor c = null; @@ -255,14 +264,12 @@ public class GTaskManager { mLocalDeleteIdMap.clear(); - if (mCancelled) { - return; - } + if (mCancelled) return; - // for local deleted note + // 同步已删除的本地笔记 try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type<>? AND parent_id=?)", new String[] { + "(type<>? AND parent_id=?)", new String[]{ String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) }, null); if (c != null) { @@ -273,11 +280,10 @@ public class GTaskManager { mGTaskHashMap.remove(gid); doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); } - mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); } } else { - Log.w(TAG, "failed to query trash folder"); + Log.w(TAG, "查询回收站文件夹失败"); } } finally { if (c != null) { @@ -286,13 +292,13 @@ public class GTaskManager { } } - // sync folder first + // 同步文件夹 syncFolder(); - // for note existing in database + // 同步数据库中存在的笔记 try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type=? AND parent_id<>?)", new String[] { + "(type=? AND parent_id<>?)", new String[]{ String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) }, NoteColumns.TYPE + " DESC"); if (c != null) { @@ -306,19 +312,18 @@ public class GTaskManager { syncType = node.getSyncAction(c); } else { if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add + // 本地添加 syncType = Node.SYNC_ACTION_ADD_REMOTE; } else { - // remote delete + // 远程删除 syncType = Node.SYNC_ACTION_DEL_LOCAL; } } doContentSync(syncType, node, c); } } else { - Log.w(TAG, "failed to query existing note in database"); + Log.w(TAG, "查询数据库中现有笔记失败"); } - } finally { if (c != null) { c.close(); @@ -326,7 +331,7 @@ public class GTaskManager { } } - // go through remaining items + // 处理剩余项目 Iterator> iter = mGTaskHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); @@ -334,34 +339,32 @@ public class GTaskManager { doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); } - // mCancelled can be set by another thread, so we neet to check one by - // one - // clear local delete table - if (!mCancelled) { - if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { - throw new ActionFailureException("failed to batch-delete local deleted notes"); - } + // 清除本地删除表 + if (!mCancelled && !DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("批量删除本地已删除笔记失败"); } - // refresh local sync id + // 刷新本地同步 ID if (!mCancelled) { GTaskClient.getInstance().commitUpdate(); refreshLocalSyncId(); } - } + /** + * 同步文件夹。 + * + * @throws NetworkFailureException 网络失败异常 + */ private void syncFolder() throws NetworkFailureException { Cursor c = null; String gid; Node node; int syncType; - if (mCancelled) { - return; - } + if (mCancelled) return; - // for root folder + // 同步根文件夹 try { c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); @@ -373,7 +376,7 @@ public class GTaskManager { mGTaskHashMap.remove(gid); mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); - // for system folder, only update remote name if necessary + // 只有在必要时更新远程名称 if (!node.getName().equals( GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); @@ -381,7 +384,7 @@ public class GTaskManager { doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); } } else { - Log.w(TAG, "failed to query root folder"); + Log.w(TAG, "查询根文件夹失败"); } } finally { if (c != null) { @@ -390,12 +393,10 @@ public class GTaskManager { } } - // for call-note folder + // 同步通话记录文件夹 try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(Notes.ID_CALL_RECORD_FOLDER) - }, null); + new String[]{String.valueOf(Notes.ID_CALL_RECORD_FOLDER)}, null); if (c != null) { if (c.moveToNext()) { gid = c.getString(SqlNote.GTASK_ID_COLUMN); @@ -404,18 +405,16 @@ public class GTaskManager { mGTaskHashMap.remove(gid); mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); - // for system folder, only update remote name if - // necessary + // 只有在必要时更新远程名称 if (!node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_CALL_NOTE)) + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); } else { doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); } } } else { - Log.w(TAG, "failed to query call note folder"); + Log.w(TAG, "查询通话记录文件夹失败"); } } finally { if (c != null) { @@ -424,10 +423,10 @@ public class GTaskManager { } } - // for local existing folders + // 同步本地存在的文件夹 try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type=? AND parent_id<>?)", new String[] { + "(type=? AND parent_id<>?)", new String[]{ String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) }, NoteColumns.TYPE + " DESC"); if (c != null) { @@ -441,17 +440,17 @@ public class GTaskManager { syncType = node.getSyncAction(c); } else { if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add + // 本地添加 syncType = Node.SYNC_ACTION_ADD_REMOTE; } else { - // remote delete + // 远程删除 syncType = Node.SYNC_ACTION_DEL_LOCAL; } } doContentSync(syncType, node, c); } } else { - Log.w(TAG, "failed to query existing folder"); + Log.w(TAG, "查询现有文件夹失败"); } } finally { if (c != null) { @@ -460,7 +459,7 @@ public class GTaskManager { } } - // for remote add folders + // 同步远程添加的文件夹 Iterator> iter = mGTaskListHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); @@ -476,10 +475,16 @@ public class GTaskManager { GTaskClient.getInstance().commitUpdate(); } + /** + * 执行内容同步操作。 + * + * @param syncType 同步类型 + * @param node 节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 网络失败异常 + */ private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } + if (mCancelled) return; MetaData meta; switch (syncType) { @@ -510,291 +515,103 @@ public class GTaskManager { updateRemoteNode(node, c); break; case Node.SYNC_ACTION_UPDATE_CONFLICT: - // merging both modifications maybe a good idea - // right now just use local update simply + // 合并冲突可能是个好主意 + // 目前只是简单地使用本地更新 updateRemoteNode(node, c); break; case Node.SYNC_ACTION_NONE: break; case Node.SYNC_ACTION_ERROR: - default: - throw new ActionFailureException("unkown sync action type"); - } - } - - private void addLocalNode(Node node) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote; - if (node instanceof TaskList) { - if (node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { - sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); - } else if (node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { - sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); - } else { - sqlNote = new SqlNote(mContext); - sqlNote.setContent(node.getLocalJSONFromContent()); - sqlNote.setParentId(Notes.ID_ROOT_FOLDER); - } - } else { - sqlNote = new SqlNote(mContext); - JSONObject js = node.getLocalJSONFromContent(); - try { - if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - if (note.has(NoteColumns.ID)) { - long id = note.getLong(NoteColumns.ID); - if (DataUtils.existInNoteDatabase(mContentResolver, id)) { - // the id is not available, have to create a new one - note.remove(NoteColumns.ID); - } - } - } - - if (js.has(GTaskStringUtils.META_HEAD_DATA)) { - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (data.has(DataColumns.ID)) { - long dataId = data.getLong(DataColumns.ID); - if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { - // the data id is not available, have to create - // a new one - data.remove(DataColumns.ID); - } - } - } - - } - } catch (JSONException e) { - Log.w(TAG, e.toString()); - e.printStackTrace(); - } - sqlNote.setContent(js); - - Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); - if (parentId == null) { - Log.e(TAG, "cannot find task's parent id locally"); - throw new ActionFailureException("cannot add local node"); - } - sqlNote.setParentId(parentId.longValue()); - } - - // create the local node - sqlNote.setGtaskId(node.getGid()); - sqlNote.commit(false); - - // update gid-nid mapping - mGidToNid.put(node.getGid(), sqlNote.getId()); - mNidToGid.put(sqlNote.getId(), node.getGid()); - - // update meta - updateRemoteMeta(node.getGid(), sqlNote); - } - - private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote; - // update the note locally - sqlNote = new SqlNote(mContext, c); - sqlNote.setContent(node.getLocalJSONFromContent()); - - Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) - : new Long(Notes.ID_ROOT_FOLDER); - if (parentId == null) { - Log.e(TAG, "cannot find task's parent id locally"); - throw new ActionFailureException("cannot update local node"); - } - sqlNote.setParentId(parentId.longValue()); - sqlNote.commit(true); - - // update meta info - updateRemoteMeta(node.getGid(), sqlNote); - } - - private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote = new SqlNote(mContext, c); - Node n; + 以下是为 `GTaskManager.java`文件中部分方法生成的中文注释: - // update remotely - if (sqlNote.isNoteType()) { - Task task = new Task(); - task.setContentByLocalJSON(sqlNote.getContent()); - - String parentGid = mNidToGid.get(sqlNote.getParentId()); - if (parentGid == null) { - Log.e(TAG, "cannot find task's parent tasklist"); - throw new ActionFailureException("cannot add remote task"); - } - mGTaskListHashMap.get(parentGid).addChildTask(task); - - GTaskClient.getInstance().createTask(task); - n = (Node) task; - - // add meta - updateRemoteMeta(task.getGid(), sqlNote); - } else { - TaskList tasklist = null; - - // we need to skip folder if it has already existed - String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX; - if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) - folderName += GTaskStringUtils.FOLDER_DEFAULT; - else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) - folderName += GTaskStringUtils.FOLDER_CALL_NOTE; - else - folderName += sqlNote.getSnippet(); - - Iterator> iter = mGTaskListHashMap.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry entry = iter.next(); - String gid = entry.getKey(); - TaskList list = entry.getValue(); - - if (list.getName().equals(folderName)) { - tasklist = list; - if (mGTaskHashMap.containsKey(gid)) { - mGTaskHashMap.remove(gid); - } - break; - } +```java +/** + * 添加本地节点到数据库中。 + * + * 该方法负责将同步接收到的远程节点添加到本地数据库中。它会根据节点类型(任务列表或任务)进行不同的处理: + * - 对于任务列表,会检查其名称并设置相应的父ID。 + * - 对于任务,会解析JSON内容,确保ID唯一性,并设置正确的父ID。 + * 最后,创建本地记录并更新GID-NID映射关系。 + * + * @param node 要添加的节点 + * @throws NetworkFailureException 如果发生网络错误 + */ + private void addLocalNode (Node node) throws NetworkFailureException { + // 方法实现... } - // no match we can add now - if (tasklist == null) { - tasklist = new TaskList(); - tasklist.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().createTaskList(tasklist); - mGTaskListHashMap.put(tasklist.getGid(), tasklist); +/** + * 更新本地节点信息。 + * + * 该方法用于更新已存在的本地节点信息。它会根据节点类型(任务列表或任务)进行不同的处理: + * - 对于任务,会更新其内容和父ID。 + * - 对于任务列表,会跳过已经存在的文件夹。 + * 最后,更新本地记录并更新GID-NID映射关系。 + * + * @param node 要更新的节点 + * @param c 数据库游标 + * @throws NetworkFailureException 如果发生网络错误 + */ + private void updateLocalNode (Node node, Cursor c) throws NetworkFailureException { + // 方法实现... } - n = (Node) tasklist; - } - - // update local note - sqlNote.setGtaskId(n.getGid()); - sqlNote.commit(false); - sqlNote.resetLocalModified(); - sqlNote.commit(true); - - // gid-id mapping - mGidToNid.put(n.getGid(), sqlNote.getId()); - mNidToGid.put(sqlNote.getId(), n.getGid()); - } - - private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote = new SqlNote(mContext, c); - - // update remotely - node.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().addUpdateNode(node); - - // update meta - updateRemoteMeta(node.getGid(), sqlNote); - // move task if necessary - if (sqlNote.isNoteType()) { - Task task = (Task) node; - TaskList preParentList = task.getParent(); - - String curParentGid = mNidToGid.get(sqlNote.getParentId()); - if (curParentGid == null) { - Log.e(TAG, "cannot find task's parent tasklist"); - throw new ActionFailureException("cannot update remote task"); +/** + * 添加远程节点到服务器。 + * + * 该方法负责将本地创建的节点同步到远程服务器。它会根据节点类型(任务列表或任务)进行不同的处理: + * - 对于任务,会创建新的任务并添加到对应的任务列表中。 + * - 对于任务列表,会检查是否已存在同名文件夹,若不存在则创建新的任务列表。 + * 最后,更新本地记录并更新GID-NID映射关系。 + * + * @param node 要添加的节点 + * @param c 数据库游标 + * @throws NetworkFailureException 如果发生网络错误 + */ + private void addRemoteNode (Node node, Cursor c) throws NetworkFailureException { + // 方法实现... } - TaskList curParentList = mGTaskListHashMap.get(curParentGid); - if (preParentList != curParentList) { - preParentList.removeChildTask(task); - curParentList.addChildTask(task); - GTaskClient.getInstance().moveTask(task, preParentList, curParentList); +/** + * 更新远程节点信息。 + * + * 该方法用于更新远程节点的信息。它会根据节点类型(任务列表或任务)进行不同的处理: + * - 对于任务,会更新其内容,并在需要时移动任务到新的任务列表。 + * - 对于任务列表,会更新其内容。 + * 最后,清除本地修改标志并提交更改。 + * + * @param node 要更新的节点 + * @param c 数据库游标 + * @throws NetworkFailureException 如果发生网络错误 + */ + private void updateRemoteNode (Node node, Cursor c) throws NetworkFailureException { + // 方法实现... } - } - // clear local modified flag - sqlNote.resetLocalModified(); - sqlNote.commit(true); - } - - private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException { - if (sqlNote != null && sqlNote.isNoteType()) { - MetaData metaData = mMetaHashMap.get(gid); - if (metaData != null) { - metaData.setMeta(gid, sqlNote.getContent()); - GTaskClient.getInstance().addUpdateNode(metaData); - } else { - metaData = new MetaData(); - metaData.setMeta(gid, sqlNote.getContent()); - mMetaList.addChildTask(metaData); - mMetaHashMap.put(gid, metaData); - GTaskClient.getInstance().createTask(metaData); +/** + * 更新远程元数据。 + * + * 该方法负责更新与节点关联的元数据信息。如果元数据存在,则更新其内容;如果不存在,则创建新的元数据记录。 + * + * @param gid 节点的全局ID + * @param sqlNote SQL记录对象 + * @throws NetworkFailureException 如果发生网络错误 + */ + private void updateRemoteMeta (String gid, SqlNote sqlNote) throws NetworkFailureException { + // 方法实现... } - } - } - private void refreshLocalSyncId() throws NetworkFailureException { - if (mCancelled) { - return; - } - - // get the latest gtask list - mGTaskHashMap.clear(); - mGTaskListHashMap.clear(); - mMetaHashMap.clear(); - initGTaskList(); - - Cursor c = null; - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type<>? AND parent_id<>?)", new String[] { - String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) - }, NoteColumns.TYPE + " DESC"); - if (c != null) { - while (c.moveToNext()) { - String gid = c.getString(SqlNote.GTASK_ID_COLUMN); - Node node = mGTaskHashMap.get(gid); - if (node != null) { - mGTaskHashMap.remove(gid); - ContentValues values = new ContentValues(); - values.put(NoteColumns.SYNC_ID, node.getLastModified()); - mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, - c.getLong(SqlNote.ID_COLUMN)), values, null, null); - } else { - Log.e(TAG, "something is missed"); - throw new ActionFailureException( - "some local items don't have gid after sync"); - } - } - } else { - Log.w(TAG, "failed to query local note to refresh sync id"); - } - } finally { - if (c != null) { - c.close(); - c = null; +/** + * 刷新本地同步ID。 + * + * 该方法会查询本地数据库中的所有记录,并根据最新的远程节点信息刷新每个记录的同步ID。 + * 如果发现任何不匹配的情况,会抛出异常。 + * + * @throws NetworkFailureException 如果发生网络错误 + */ + private void refreshLocalSyncId () throws NetworkFailureException { + // 方法实现... } } } - - public String getSyncAccount() { - return GTaskClient.getInstance().getSyncAccount().name; - } - - public void cancelSync() { - mCancelled = true; - } -} +} \ No newline at end of file diff --git a/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/net/micode/notes/gtask/remote/GTaskSyncService.java index cca36f7..9512bc4 100644 --- a/src/net/micode/notes/gtask/remote/GTaskSyncService.java +++ b/src/net/micode/notes/gtask/remote/GTaskSyncService.java @@ -1,20 +1,16 @@ -/* - * 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.gtask.remote; + /* + * 版权所有 (c) 2010-2011, MiCode 开源社区 (www.micode.net) + * + * 本文件根据 Apache License 2.0 许可证授权。您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,否则根据此许可证分发的软件均为“按原样”分发, + * 不附带任何明示或暗示的担保或条件。请参阅许可证以了解具体的权限和限制。 + */ + + package net.micode.notes.gtask.remote; import android.app.Activity; import android.app.Service; @@ -22,29 +18,68 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.IBinder; +import net.micode.notes.gtask.remote.GTaskASyncTask; +/** + * GTask 同步服务类。 + * + * 该服务负责处理与 GTask 相关的同步操作,包括启动同步、取消同步以及广播同步状态。 + */ public class GTaskSyncService extends Service { + /** + * 同步操作类型的动作字符串名称。 + */ public final static String ACTION_STRING_NAME = "sync_action_type"; + /** + * 启动同步操作的代码。 + */ public final static int ACTION_START_SYNC = 0; + /** + * 取消同步操作的代码。 + */ public final static int ACTION_CANCEL_SYNC = 1; + /** + * 无效操作的代码。 + */ public final static int ACTION_INVALID = 2; + /** + * 广播名称,用于发送同步状态更新。 + */ public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + /** + * 广播键,表示是否正在进行同步。 + */ public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + /** + * 广播键,表示同步进度信息。 + */ public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + /** + * 当前正在执行的同步任务实例。 + */ private static GTaskASyncTask mSyncTask = null; + /** + * 同步进度信息。 + */ private static String mSyncProgress = ""; + /** + * 启动同步操作。 + */ private void startSync() { if (mSyncTask == null) { mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + /** + * 同步完成时调用。 + */ public void onComplete() { mSyncTask = null; sendBroadcast(""); @@ -56,17 +91,28 @@ public class GTaskSyncService extends Service { } } + /** + * 取消同步操作。 + */ private void cancelSync() { if (mSyncTask != null) { mSyncTask.cancelSync(); } } + /** + * 服务创建时调用。 + */ @Override public void onCreate() { mSyncTask = null; } + /** + * 服务启动时调用。 + * + * 根据传入的 Intent 中的操作类型,决定是启动同步还是取消同步。 + */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Bundle bundle = intent.getExtras(); @@ -86,6 +132,11 @@ public class GTaskSyncService extends Service { return super.onStartCommand(intent, flags, startId); } + /** + * 内存不足时调用。 + * + * 如果有正在进行的同步任务,则取消同步。 + */ @Override public void onLowMemory() { if (mSyncTask != null) { @@ -93,10 +144,20 @@ public class GTaskSyncService extends Service { } } + /** + * 绑定到服务时调用。 + * + * 该服务不支持绑定,因此返回 null。 + */ public IBinder onBind(Intent intent) { return null; } + /** + * 发送广播通知同步状态。 + * + * @param msg 进度信息字符串 + */ public void sendBroadcast(String msg) { mSyncProgress = msg; Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); @@ -105,6 +166,11 @@ public class GTaskSyncService extends Service { sendBroadcast(intent); } + /** + * 静态方法,用于从 Activity 启动同步操作。 + * + * @param activity 调用此方法的 Activity 实例 + */ public static void startSync(Activity activity) { GTaskManager.getInstance().setActivityContext(activity); Intent intent = new Intent(activity, GTaskSyncService.class); @@ -112,17 +178,35 @@ public class GTaskSyncService extends Service { activity.startService(intent); } + /** + * 静态方法,用于取消同步操作。 + * + * @param context 上下文对象 + */ public static void cancelSync(Context context) { Intent intent = new Intent(context, GTaskSyncService.class); intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); context.startService(intent); } + /** + * 检查当前是否正在进行同步操作。 + * + * @return 如果有同步任务在进行则返回 true,否则返回 false + */ public static boolean isSyncing() { return mSyncTask != null; } + /** + * 获取当前同步进度信息。 + * + * @return 同步进度信息字符串 + */ public static String getProgressString() { return mSyncProgress; } } + + + From 9b1abb5836b7437ed1802619abf097c097b2e8e2 Mon Sep 17 00:00:00 2001 From: fanshuang <2963071932qq.com> Date: Thu, 26 Dec 2024 16:54:05 +0800 Subject: [PATCH 2/3] java --- doc/202201012005范双.docx | Bin 0 -> 26557 bytes ...小米便签开源代码的泛读报告.docx | Bin 0 -> 54199 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/202201012005范双.docx create mode 100644 doc/小米便签开源代码的泛读报告.docx diff --git a/doc/202201012005范双.docx b/doc/202201012005范双.docx new file mode 100644 index 0000000000000000000000000000000000000000..1ac69721841c7d4756631f7584300adb923eae8e GIT binary patch literal 26557 zcmeFYQ?RJbk}W!I+qP}nwrv}0+O}=mwzZ~h8*7?(?Z0oI=o`K7i8#-F>Y?I`sC=o6 z9F-$8$EP3-41xjx1^@v706+*35OTa0uW zVQWKB2m(Zr4*>Mn{{OE3!5(N%k(D15KnT4h`wla!3#Id57**hTbyPuOyzjh{Gd%P) zbr*Phk)vc8B@iu0BfFmNzHM4O+TOn|V_iowTG&CgiGR@qlk+Ost~RJk8rxx=LV_v_ zzK4Tq4IH);KQ$kkpc0t0*eD5{K^%fTm7E=jO#T3fr7DS2Bj_2$gcs_K!n~XE{fU&m zMZkHkUoa@z=S?;;Wbl41o@12+Nz!mrXIIk$MJB|c1y#zS&w1Zp*yK23>9~<}#$5nq zW$0ZjsQk36&+sl?7O5e?jtZ9_R6_r`Q*=B&9$YZ#)i(n#qnD;F$f#QqdPGtx%+XF_ zN2PAwe7HS`j|$%D)f{3C#1+S1*t5tMYhDNn$I8V0?u2fuR}L5PdpBiSs6l{K2U|FY zwAyy_7AOv&17Mr1fxu_*IgdWb@}QPD=jDpi{frUoLCi+l3R zXEV0I?e)_hkPsUZMX2THndk#+jb(U;FkGcKqG}&T2cxfHVo5uxFNDNNlr*X{(Lr3J z1zv5b5csPH*~=o*iLo_9f-fQ5!%7-^hHo{>Z?k1x#@n&a^ZR`{Mfr^Jlp z%{-@(r4jg9?D(*mbr0DyPn@=obP;Vo)u+sVYJ!vEH2Z-69bC+7ZV@wy002Cl009vH z`pDhh$%Nj--q_XlZxZ}h_Pg@2RL0g!J$s4p_z@h4N_3E!5BOm!(Rr-+9^A@OM{*+Clagv! zlN7~FnB;jmS-;^t&2_%v|M|AE=W$t=C^4QGK8JfinMY(oN-$wbJ>%QH|8jp%6B4J$ zfg~^7_h2&2zG*ir=Jx$r{(0~6>s>&V6?^xN5g`K+AX(5=eAu%Jw2VFL&#%0l)3d)+^4&`h zxYz4I=0=DXXIcxG4x$c-97{^a%1C^tf+|FhYl{K2yPg$|<;bNKkjS$=pC{##lK0d2 z&Xeth?$!HQ^&X5khK&Y8?uMn#eD6~FU;_p2e&*Oxt)WxxS{lvKZ_Jc%6Y-?lB5dFE zyFZN=l_2d!xc_*^NNEX&fQs!70P~aMWG_&YFewO>k{c|LcvU)5p>G7r{#kayoj)dFESDm^kED4`-C9D^n| zTIC5d*pelbr4{N<%N`c?qf;AsiN84U%ZK~7QH>yXQ&D4`z+nZThG{^Zs?hlgnR6}q z7C4KieetD?J5dIdes~Zj;5{rLBuod2{|}H@Xx~=csOk&XQQkvXl;bPm27Vw_!y&Z+ zh@OO4-N3HT9&~9HeKu4cAd?7FWMx9}%Dx_`v;~;n@f81_6*>b)(QfVy0Yr`grP*ns zl@83y81o(W<6-QAJKOO$S6T-e_~KP34%nx-tIJ@NUyKT!h<_@((w&w06XK)%TT`x# z$->@Et_D!FLGU6bB`$OtKE-bG+oaCSdYvA6TkG{P?H}QljdUtB-}*e=Hgm9ScIlG+ zY)&Q<-i*dkw3$ugy9pRCJlHO5Oc3g`>e@hyUx z1{8&`7H(|nks z5xdWlkiwrz7rGGOAh9LBpo*5xweWP$uCX9W6fsaHC_MbvDrX^v(yC zDUnb{nUl{e6&|k|l92pdPytE0gd#=V#G4h^q$H_DPmil-vF}0xmg-803O%+DL**dv zo($C%mF2$Qf=8K9+hhI$(?1xdcezVVFV!`r2s^XadELwA(b1cnz^0TI6I(Z~Ojt*O z-&f(M>h(EZS&wbhyz(?>k8kEMoT20@md2Y3Oc=>i*Kja#x)^$|mKP&b@H?#wqrx#W z{T6*@;HBiM-1SmiAn)L$)yIAU7)>L7B1#LY(egwKKV7tQE_`gxbg)7O6Fw%+n=S*Q z%%3u0083h0Bnz*id*L`w1Ah1V#A=#UjL<@fjF5Qgj)d|&nq*O-2#n{PSbn3)(*O~` z4x}}Lx5LVIHp5wZPwIS!*xVGfI?&8_2}h@q@2HwxzCTaxxy0GI>5)O-i%*w^CbOgA zU?ccz3fkEU1jE>WX{E`~*#kl=-nq)4XSj&7VjiUiBKIlrlryIkk}1Dq&dcJCSEDZ< z_iYhqhf5G5THpdk+}ZHE#Zth2Kq|K{MQBW){QP z$&9%4%6QNa5+Z5t)k`S&XBy(PYLYoe1R-VIQ*AvF{Xzguvmmu_*9z9x`s8uE?#E`B zqxmbjvkjyMuUxMUFX16XTyU@u5*MwodR1sJ%@jj)|Lvu}&ru&9b|+r!qRtSp>%5~2 z^JWrc1u|lUfA(j?T&N{}B2LL&CwZ*E6jfQn72F(H=GpH92w8vHv0hX;<|nWI z_xKN1?`=R!kU-$o{ZTQ2ec~8afT^$}5V%RLz5Q!xd_Rh#mj_3>Mf@02pY+uYrr)qNL-&rH z&a1lK&#kO)3tR2r^$lqAKxRhvJFU>p2KTOZc3kUWD>H)5XSBIa z#j3g_8r#Uf%v}XnEch6XVr%Z+Oi485(XTtSSB6BLKMWnccRG84lBgg0N3#VopdF0M zx>@~E3k^8cOh8A6Kt^+f3(HWIf)lhsGh(8xo_j z3hZO17!M~Q)35FE#Tb#{H6l|~RtqX;h>1X@2r3Bf^KG-bo93h!(%^ zf#KGbMV*|0TYCa-DZ^T8byOUzRA*#S^k4*V8il};=h35~)p%brq*Rl!LM_&jfhwku zwx?-dsskHAHX?ZFir=_82Gc-8(PTk(*M6NJB8Q0W2sTxk@iy-0wWZk!R(&1&Gy5X1 z_AYozBJg~G1Fpw*^?ZkEoan#P2hTo_9U8d)X!j*N4|(8w?wJAe3)q;($ZfuPI)Alf zXp2T@wp@jFY|qAbOF=Q%_p)h>N)(qi;+rpme;xz-alWZK|KTf)*KN1v%?SEledj(M zalKrQaf0%6t8dR?M<>II@LC8ige*Rrsa#|YCELCp>qq=x!C^cxv0;(hVh6YX8$z7a zFO0lkDT-yw} ze`$RFTDSf*W^$rTy(jdDvF>5ap7x#+@eoD8RDx8>**pd7|L4tQZ-Q5l>T#a7*_8FrJ-nzz492z`2Uf#@$7np)#~{Y6rbE&B z&h=bx2gmzjYil@}KGuL!_1Q!O?2-HCre2;+z%#1SMm4r*%`Br!#~f-={eUw+1~U#V z-@gcCYk%(d5)Y-$QPvZfn)l_(;=zjX>2=V;Cs%cEZijgvMY85ZjYhvQsT>N0-ZHNq zv|xDRk?ipdRfw5i><>hTU%re#fo!ftEeNdiO6NK}57i~K&Dg4))m3)zEY z%=E2AO#jBs7O}4&p5)}cI*LRZdFNZUTIMdZ`}Q5@mH4LrKb|yYly>C(xcmc6=TAXm zI><4l#$oH+mY5?>L83r2!ZE+TISHLe(gng}nX=XV|81NHQPPGnf}UZdo{q7m39~g?y~C} z&H4IGtJYtX<*Zvrqm?rXG$_*zp3{%cvBwsQPsM$XEW(x|3acmlbp^euvs371H?2<| zy<38>BQZ{?qIOUoeJ4vKg{If7xMpzzV&*2P_mysMh`Q(f|1tnMiztq@)yR z#PM=&Nt~j{DWR(E98Z_iOx@dM9SbnxW->3vmKy4Zr4%2;{NSh#F+?V+rZ{0gEQ$G9 zz^%lCyoFvn`DVX)?lmuYahy!Pi*p&90i=H0bo+k|tA$P{B>a7oeZaN`$CKxCeptpDSrRNC+}d_K zfTb6=93Uq#uqBUVDoNb!vn#2GgqjOeXkc6Zyu~>LcC!<0mobFh!quzgb=|Gc*anv0 z^!ORU`8=Jg8C4@>=l zl+M|YHF!lmVHGK&kb5mv?=sm*2Tr(k*t>jaTxTvbl%^sgmdL{uXF#F&dxAi|&=yY4 zcW0KrI(JhI)z$jfnipF%kzek<8>Z-Xg@e8IXc{Mbpx7v8CF&n&3x+k*Yt?~?zx?d3 ztcM3`*NN6X_DkcF!8VrP;XxaKx*eAiGZEE7-XU{YGto>}53c=LVw+WI6+-o}+j}&x zrfWs!{QiD&>cP=asCO-P(z+k|kRpM^m0sVGXDEUN-ls|6X7fCRP&9@@{i$z@s{wFJ zmqLylbO2edgfzjx#b=j6d(W@~cvW<4e#S-~4n;mV%#;_S^~6u$9fCb6RzXD9zgo-M zBOGK*Qm6jSWow(7)$=?NUi|!k4Z%oK^P%$qqyiC(8q6EY*12X229ji{CMab#*Af?E zfw^4V_;i-zH5!fs%+JW|OrpA$q1l}A7*j}@l+|H7&UY?y(KC@W z$fgJQd~uYwifr$-S%LsISOni4Sxc2t^3kj{oJe}&qwO0Gqm(jXcAIV0jtFXCrN^#H zpV@tA!swYQpCp5va)NlDrUoLiAGAdjL z+f$4THLxJx5%n%QkLS>_+tBhN39zaD{x7j^fdf64H}_uC3v96}&l9^&fY`(HK5xm$ z6J*0Q>y0H*AYsb4atCI~MIR?v9q>E;{W{AV~+ov*!gD6DP~66?T}^E4Ey< ztku%(Sxhp|)kj@UC7IkGKfuWG-Lw>|=3W(BMIy?zr8S}1H=QOgIWDW+WG}_e zo|BbTas~7Ixt!9H*wiX``m9pQi_L>FB*h}s@^IrivrJs3am7+~3*+TjL)5*g&y~T< zZn~`FHQ6J64h_1^AsMD3fvU)}QcxrDkWYQv@(3*WO`myZ-Fv1(hzE z{*-mG({wa0Z*R4#jUKY4Z1Zb=@<1#(Rh?6)*zT#!y#9xAS2kI?CTmdEqGqG7UVGO? zT0_^ztu?Vaf0#KQmUyBQ80XL0{iL*_w6)v+fo%u1u1-aw`Qn(`|D6;^sjCK}w8*yt z6;0O=Jbarai#R;D9dKEAcxDC$DR-@<8oN(&>Fni0c1)pKsxgQySLiCHxNUsIH&-%+ z&#S20Yml=tN}`o!z%b6s)XmaoH+NFQxF=ejz7D%bGPqn}Rv=8Bo}o>Nh8AU=bh70C zUZvH7C3V%dR-mXfNjd_a>5ReU;4V|@_D zNMP97pYKfgGQJN^|CuysCai*D%mfdC^pvPVBM%R7S3#7Y&* z%n5@Q0z^_!u!glpBUKgkMWS6`&%WWGwtkqh%`y6t4yU7k`15bN023QCquxFL} z()E(T=d$`FYD?XnTN@lyq|cRrFXTSK)r9w#nV&~=cz6fDxO6a)m;AVyh-Olt6|SGF zf~`WSQY~{36GBc3x|`uRGQfsq_1#SkuQuOz`vwLu1aPMkRxSfN+3ysq0W__Y{24uu zLo~O8>t&`$qU*}FBh1{BZLeB?^v?s((_??mhw)E`-A-=Emh0d7JsIridV}q|(*oyj z06bzoJ}BE@!XcOP31XqzysY|$SXh~}4}~x3CVJ?qVh-FXzGlI;?MsXJ_dgRE`i)A$Ka zvAMQZ(Pv^_p4Yhl68sY~+OhD$Be$_S*=Yh#)x<#N-(ReJ)7^X7PKEl=P>#Ly|X`EZm{a ztf#5d%h_KStSTi_+9Ry(hhQHHo>sS)Q5b}YFa^qZh{T?`$tbK0$v&{t$KpBoV1^e7 z+O!}k$6JZ4?0`!JZx$8pY@v1*J_Pu}y%Y{HWH9inx#L;b?m5@wD@R0jN%#q65VJ{P z;B-{gl{XB^{rWYFl{T!XcEHkDxd-*FidE>p+fK%-HP-|_@WvwNe?KC0aVFi);5fjb zU(GvCpTJcL-B<3-+~;fN6fNiPtgF-gpXU)gWI%*U?B`3Pa;f8i7NYjx@n zOVp=KFjXD({ssr^Ds!;AJrEfDY(h4x?kHRCeO!^6)p?!A@&w@X@NX-k-@JZ+gy#@$ zNn`_AA-GI?Gp0ag7{Cyvw;oJsKj!Buaf%j~;LuG%p|Uu>u8ME3$vc@=f4L5U?OOrB z*Mtn|svcAL(f+5zlE|HJuF5%-cGmn?d9c&j#0%Zi+M#+mz-89iNhA?S;|X7t<{4XB z%;pwC7MJTMG}5BjF0WA6w>2?aA`-?=tTr&uUM$zx?5*N%3(&2XkVKB+rts_P(7v8& z99vC@JKB+9{$jR^_2LS+F%#=V7371*S!@}reY1=FS$M_EdkX6dk%EQVz3dl|GHK6r z5;Uv}E>$a4-rVCIuGbdMj)1xX1cV?R?{r$O{Ov3Ww;e^@NooPiotV=UL}W4Q46GV1 zKBdItOJSYy{nY(rn#fZZ^ZaCs6i?l|0P;fFri>bwj0Nw;i4I^0||zw-7}aEj3S#cf>pCpiiX< z+UlgoU7BgJ_5%QgCuH0g^ZkwJtqq+vv7LIwmD)`c?>_JgQ@tB!@0{pDpPSk|rZa4h zW4|`oX0(X0_l4IimY6~Qq;dZz>StGocy+5iPH#_?Cix<&D4O~XVAZAi>eUt3q8qrF z37_?5E!+nlw=I~W2hA$ts+KyamC+n7i?)T1Y!+AN#c$Bo4*IN=xzHKIE#q+sRWr`n zq?V~Bn5qY;8!-diT5h_As4s;pwP^NpIXpV{{*7S?va$b^($Y9-cD=Ok}-t1=j*&_?C{DgV5_n_J)mDrE9J`zBw8ib z(m_QC1LDKN2_+c;qg}bYsHty zQ#kv4`{Q%Z;DKQPDebq=P4oxw(m4Ma=QA71=jWPTw;)%hw63a&IX61^kaxnRRGI_= z2za-egrR#9EY|C-^h7T7i_UXG!X@OJFZYc+j?XG=-Gz2N?_C;;PSX!qdPSef!J*nv zyCFYcHhe%Hr4FKz;_xT|r1JiA%8OlWQAhjKGqd(urLfQTqR7FMQWT$Q;@DiT?#^j)ZPE5%@oMbtC4YV!?m6*Av z=Hs&k)SV9eoFCK+mTp+X=Hrj?{t>>hpOhJlz2AZl?ASYs8E1w8s=afJ|KJnFXWeLv zFyWo5z~0W9KGL^B`o4-HY0tJ2FcdrfM4?J%g6qj*H0(_QnTetr$!!D`&cs6}j*C|% z6XKyX-F;Z~TjQ1zK%0b`A!H~NktTR#J3XzA&bvotbAY=i1ohCqXmn(Xv%BIhit|^_ zlg-*homQ4}y@-!lenO#+TB&JH5s>_o!fR7D_};>puPQ^{*5D!$%GgC^K3FM1Xu+MS z#LX>Sg%}uzQiDk$^<&w$2hUPkFrEz+h`TO*;14q5Aqv<5W~>6Ka(ufU$k?%@8i|R4%N35H zHi6LbJ<;XC=l0f#%$2=H#{9=_n`g?^(=ivkR!60*C+ zKaF|*`LcoDVc`RF?_oZ8^PTE9-G_wE@hO7GaJ!YrJM(DXN6b>ov-3n*0eRn`k((L; zMIarWbL*t85$HC?ix!F0R^l6vN?ds=X0b4-S2i4L>Hw{P2=J!6?sY? z27(!+Z$0M~NN^yZbE1IX{|(vsTCr%pGxla}?(6#vuw2cV$mH?`!Ic5XMa_GY7fRw3 zDK-qpCw*1dvwe*%hs$e4j5Sy<*H7X-ejHLUW}=yRDwAgaFNQUEbNN^j4JD+<{0T}h zImbK6`@I%T+H$qQesVot&KgN5`mR$iKf12bZjm#jHf!&sRMJQ%Z4odTkc{v@xB1}z zuxkeocq+Bh#U%cvY* zixl=dh(EZYIP7I=SKfaI55ONIu6F}?y5P4uJ>Yc~p$o2>sUjL`=ngiUoUQj}e{kfL zi6%hZFmktlD{!+!5Qsb@+e#cVTeXP10(k_Xq^o0N@CtSWu+;!#)*$m1FQF zMjN&KBCyOjrGV1v1XTAC(-CEoQ7^}WhJ6Y~p68H>q+(V(o)ON#BTkkX=`@aT;4x9T z8QZ7PD`Cxq#CkWRYr?=V<;*n^d04D|-absR6e{xEKt9}8$v6ziL1a+Oi15&dqicUz z!uo%QmFcN0Gm-#tf;V#kL^o4_p>b^20q)>6FyxaTbhW5BtMhbrGMR7WD?{!nc0q?D zGlVGa6sG3c*M>)XDVi?NF`VrxUY|G|CoSSj#497t3y^^dlCj?`bIrVN)!H|jHw)nN z2!C`q5|bs=;DkOs*oH$KI>CXfSPJ5UWxPD{_EB^Zw~C(us<^Yfcc01OYF2-DNN^-r z__fU9)p;|fT!H!;+tc5H3dLh!R1ZT_%}@ay1qlei8<4*EchE0}j~4h1_0v^Fkr0Di z`%tm7s?$TK)(ayTER)_=@VS%@^kH4oKS;~-|EUNejk z=3L2(Zp?9o5ka4;9AQKOOgIKpTn1D`lLMK42a|u~UdWHgukzlY!{4(MF$E%AdE&Bf zBg?yuAziQX3}BAnW*1k0$a=9QKq#d`s=$7P%WH6N#$EySyCpF-QA`4Em1@ z8adj+v~;wXgH<1oEXYHjz*|?hQziLw`VKArG~L?GgDSl)dbtu-_7mdo-wo z7HH}v^Gb@3m5OXlwrO>i6qH$oXwpy|ev5&KDu6snp9+UL}Ef3Ith0g=H*iT%DTVt3>J z*Yv&vA0XH_UFdH4=dVoVzw5qW#Ln`W{|Y%}Kmh=703ZPW5wiS;`0KyQS^iT92KZOe z`Pce?_Nq>vk{e<`5PgyO6h7j0RLbX3DZ>t4qD6_QIh4U8Y9;mtI=XErzbI{P)bQY3 z&Ge4rF|J$LZItI5Sd*g`dtf1kq$sG8V~y^%&8xeSG)kz7e9;0VM%HMgqoY@&|LyD| z+j&Y8=#+H$c(Bx^{Rq;=jn?wZ-4c&T7M|)F!33{Rb97rK#&7hX z1Yy`#CY+u^A8=mQFq9Eln1Ed|Zya(&50a2)SVUlz`-9APD(5UM5brBs1peO!-NDo_ z*~VX?p&1MS0McKJe}c};-rmK|-o@1UU-GHx)J>;Ng1=fb?nU?CXFG!6_^b`gF}m(f zX_gz{1Q41sAQk|c&ztBjc1LC;$ECdp7_WW0Xl^ARVbHSG%xH-Ytu5Q!W33FrJS11m zEq*^-vn#Ens3wFm97Eb4ktJXCAMeL}@ID7M!c6)XS(g&4{dw-kbb z(s&iz!<2i9A_!od=!ADTtCU_9dl1qUus4Lfr! zfo%Fa;UCESAK`f0;vU3{Qu{rYfoI4An$>w1jIho}Pf`~p^zxcB#U4J2pIDS`x#qt? zPbtm3m<|GkN`4}d^A);H&t-y?%g3oD!#k}?o2nV(>)RmlELDZ`eK|7gfs(^G?uRl5 z+uX=B6wb|KcG~fh-zzOqI55rBvNi#JDMqNYsKG?&Qt~?_D^9B*vJyBH9*$>3OKzxu zbETA$A7VLuQvjG}E?qt>;ig;tP}zn(U})_jyi_$6NuIxriL7EeWP;68gtp(c&1@i7 z+B;Z#C|Pg~xPqhQVeLHuNC$)W(pZgl8UZYswe80TSgJZtDl_6NfUV$a0Pk`y0tT;B z1fQ<@rjL$u(_mII;DuGqL156TFoZTrW+r05(txl!?hvM;)9ja5n1@D;f!fXk)HOSY zo}W8MybIsa*jgSrHcSp+94+9IiHY*R@5G$fXb7Pg8q_i7U%Mz%?VI* z1UjjpZ?uf0>qR*{`BurHV9a4?l96n7ojhF@Y`7Ku=%$Uhid5hlwsvuj{jp7b<0 z$$iYfy@3~>+ua-~_Z_2R?yU3`tK^p)%G2I-R1^Q1*Jc+6#~Gke{K_zAKB7PQ7!$R; z(@YmrnMq(KeOgFopdD4Hv`-YkzPb@?foR71W5K3GONov$=e5WV?B7f{*m#ih^uUKP z8>2y!GqSV(i~n=1p}nIl?qK1K*xm2nBEgD)((zwNF!+mv|D_`|wKMtu2SQ$9M2_TD zX;PQmA|HU@nh}uz@OW6;ZDA312c~341w9DZcbusq`pu0zk1L#tCE3X2lReLBLEw0L z#Mk84*QI5n3$AT_xFZ#)Zijj0_Q%sIz57^rq=tlW3ot}Mk+GPXoskS0VX>qT=L&l^ zB5BK31~w6AViztk3T{cH;@+E{;#4w;igmhu&juc;7{e02PPP`JH#d}YkMG>Gt6mYd}5!sO0SB1cwh}w0sS(9A5Wt_B^|&rvXjm{>5B*JC^bO#F9krj zM&Q=yaZI#PRK2J+uHP!Cx|Y-qwi+nsb=%?uPBAi^!31QPtLqacL&&h@koz#w0vxc` zd(AC$Y}n#9@6dKqTTyE@l$+J~sYAdmKb?=gb`B_i!Cdt>T1vZf$ji2!hS@@yFU}lR zXWxZ2hEzt9Q$~jtKq3>AYGi@W)JLtjmtzHM#eA_qSA-@5N7}cXSWl5Nz#@A(8uoO% zhq#X*513u}L6^e+G?3TUo&(o4yY_tlTS09aEzB?HzZ^se69549KU0E>g{iG6{XaY7 zKcZSq?I>(^6kqyB{%}uwo7;CJ`uk8_{Y17d$#FZS2M;%~T*Yck*u-w^zL28|6sVBq zqk@2iG`{2o0btl`;UDQXlH&8Y^5O>GMB1j}OCgGNng+UE&eKKCg)+?BlBpNv$XG`7 zVAGs@w*oAJ><}6v%`Hefd^o;N3<5&JDE~$n_drOrf#Yj<{&o;;rU6Q^Nj_xl-;hX@ ze`=&egRL-zK~oQ9W?(Vp1uN;|s1d`EkJ(F7Vys#;B?C+VCka&T>|w%4hR^2E`+B}_ zCh+0;)cXdWX~gm>f0!sA1ti`AV*PQ2gJQ&3zX-_+r8&94HXv2n!*g&BK9~ehf>n@n z@~wOWC!b3ux56h;TT{3DKV_~|>@@1wFJ^K&{DAYFFjwuLvc68X@&E-e zDpSyObn*sh^9zVa&_xpL~uCWF7vMF#FK5IMXt;H4!Uz!4 z0vo=Q9vR%n2MC3yUH9tb9!}kB4_FU#*BJtzqHr`bO+v0QaWt=)JEY@7r@YWC2)2|< zTD~s9PBKm+u{nE+n7U-;1=BsE%9EQ@lOOms(}nZy&={Rk4s1SOS~{0yH;^%8MH*xA zlGsgv%r6pnG9DKsy5dTgXiDKP28(E; z1;YKKo~DBB%85B82crH@XKhLoX}dg6Q5RS&oA5kwTDJ}dymV1mJy31s)4HR?4>!=x z@T2Rp)w6-%?|Nywmk&RBY3ezrPgRbT{U?gQp8IXLimCH@!-5xpISV}GG0xGfJm&>q zGh-Pu`L9kRtmK9`+tY(AiD!?>M99D)kEmp3M8ZdwTxk?$gS|!=hV*v50%I3_&`^t7 zHm<&uF#T!9s$$;SJFQF@H*8Yqm7HHn(X-HZFad2p*hs1k=ptm{BUWtgV35pAtqtd& z>)Xq>>r4#!N>vq4;RC)y3l-@xD!#x_#5?6ezN$N|>mDmjI;VE-XzFS@YReYjAg#MP zNUCkq1%lDTR&_4jG?l_~0bDgvakXFuDo`Z;qjC-;X57x}+nmG#A zmWt26?Nsp;3j6^nWsjsi0o`gob*Xm6D2=Le`dA#n8R?Wb?RA}FTR={=sBIT`tT|te z`?*PM(@5#Ot*0vXR=nI`uF-at&i)aqFHaf3vC3=+)8f2P(mn(ZK(*^-Ro_Ou?1e>h zujwT)n@cHedjtn_bCJLP%=Oa2Ri=bd{iWYS>|(ADJD1X*Zuj>Gp?^~?lIANpvVj2r zxc+7B|BJ3To4UAI+L`~u+nY5u?KU|OeDn=|2JZfFtt_)IQQAyRf;w*2B!W#~)6TPD zN1{+n#x||_?GjSHB~pkY+2~_g%}(z-aAae?ES`=fv#n?x1veQk@-<|_P8;PSwBoSY z*AA)Gt&fu$K^A838#+h*IXPYA|9Nf3JkpR-n8Fqqco4*b&!VPm670$uqs}h;3^q>( zY~LdiMdBN2%ptm0FsVr|!%Q@hqe$3nHP*~AlN<`05vmuIoR^ZM@t8$d%7A$_5;W5J zuAmv#er6>DjEh2Hu5h=3mKgw_eL$Ufh@}0Z!1k|-H2JC$u2$AMx3#&19ou$DxT4uP z$YmM_iF3>y0xfBFim(3~-GMIL-XX7d_Y{!@J!vgb56k_yC3et5a54IP<%`vumGDseS56gb_5&YvGyy@Z>_9oqC)wm&`ZbBE&z*;18&Vr{ygNbe-H-IZ~4Q664 z>|OR9BCY24h{SKz5oz0@q0r+0J5NZ``+&#R1`HF8vD>;ltaaIj+5k>ZI05Drovug- zLa+V^hd90mDlB^nDYR`IJwXKN6Y~hOR3&XS-3a8y9!xqwi6S~hWRt`r0E((^o+pt9 zxP!c*>Cf#^4w#4L!UuK}TCv_*0OhDb`~wY!vRqB)A~TnB-Cwxmc<&ozqFPitd& zd5o{Gsfns1!hnHxskZJ;%q zh>A1k8C!@v8UM|~)iO!Q$8mIUGFc2$7(T)g7_Z2)BXveLvQ=#-Nu+*4r6Qzj*gR^C z!heK<_bBmX2#fZpMNn9`4blAk$QWdYTQe@?JBH*H6{_&iG=7X~_==NsETva({iFpW zA3Jo8y73^CiM>oGUfMOZjv>|1F|Iq7dk<1IObg=q@u3c8YL;4|+=UoM5~MS7TLjMvanxX&WjWO%TcO^qD*Jm=lt0!VF|8tWysH zHD|vWO@pS<4+bJN50eXn_^as9#%ld`1D)v#ExF*kE57l5C|!DD@sDk5pvDh8`#Rq* zbI3S9f`X_3O}tWv$&8C=Zs+cvqZjkX{R47xcYgEDuFI)MzOTfcZN0Uo&GnO;^OJkN z*MuG2F}m@IuMDlZ@q^p_`mHwu{`A76?E`L=Bwf>%!2CmSWMq<%8^o!Gpn8~HMx!o; zA$>ufHvAQ$g40KD*KlL+FW)}206nWXtAyOsu48LnF`eD4=*xr#=0}*yX^YDivy0`FL@zs*qbkGx%4nrM!C)gJguC`F*LvO*>(dHm|ZVN~hJHuDCzPkF8&sM4^N1#gDojxcaK~S)LyjW$lroc~ zdhaL+FG(4mb!<6StRqJ;rxP&;*?~;Tn3t>A%u&-q2DRp@J#?pwr=SKgz5xU#JYH&@ zaa<2IVM>%YcMjY1)G+0i>uXY!-JRB4P& ztjTYJP9Mb2KUR?@)pwA310z56{tOuh5uB^U$JLXztqiLr)(xRNzDgHlt#LZC!^ga= z_B)AszehkG|80>q`%?58;h3F>gXIFI+;vpRZgoW;vz~ilhJM#>pvpHfZ^CtxC1mu$ zy~cjW-)jCut6yXMaI-FLiM{oQx=$p3F^;<49BfVRIU zgnt{2$N&@o#;(pT_O=?fHuRP*rndhS0tAI502KKIe~te4t2$l~F7R)su1?4cAd*f4 zp+EjiO*XULNqG`Lb^~)E7R=s$`3zD=?&K@&P5D;swvjv70usAn0y;r-g~LJ|B1QnI ztM!Ijw9SCPEMipSDJDH5O={A@?2hZC8K87qV!&F?IGrS>h_#;F`Hw$Mo2cUC&hzX7 z$|Dp1u=q263J`dxp~9!b`IWW%O|q^hA55xp)1tP6kU8Q5YbT0FfcQ0Xwnl<(FFl2* z_LTnqGvvQZx|j=7p$C8OsruI+82`;53QqP8&i_YF$&Qz`9%6tAy$$*XFY2-`ljc`C zNOsZ7Uu_tytvw_OU4v_7W*hzfz;U~X<&F>+^3qeu<(m%QNrB(c1Y6GpQc~+w+sqw! zY8e0*AcWeCoYx%b=i*SU3TuQNQxicf{BwbnB3MzN+~WYIzgfmpXQ{Y1L_FiL!(y&; z^^Eh!tI22fIe8`74_L}7YZe4dIC?Q5o&elbM59Y`o#iTbx!M|;I{l?B|8yJEW$g~xP{v+By}*S9&~-@~drghA2UwC&D8YgX zlM1c13T;_G5hy0*I;~Ebc!l7XfTvO4DIy-iSZa$s3Suae;`sT#-^RtyXEx2H$h2u1 zgPTGOb>(9q%@E^|TlFyd_;&O|MoE&wN5UOK(MO?YqubbbegBxTn{(DMGeWO36OE#_)19#Eq43~GX9YY(?lFjMI~ zZpENMHwVo^on_90K}O`gz*MmhGn<>Ofvf`yFbHp26=v^H?5gOo3fvGNcq|im4oT1^ z{RvCBBBR6*bIL4sz^Fi!YsTE;8G*r4*HIwZuhYmvi74ag0dj~mX_S#5()Z_lVg)0; zvh)}KT}7rqej^JZGifZ7t)cunI1ZYn{#AGJJ^AFh-bZzTm+pd%7Mhjus+lNr(OVeh zX8Q8NGf8RIs4#<)?jM_NnnhA056+bg7K07Do{MZC9nrd3qBd6Z+){My(*fgcfc{d| zQdIo-$upnnrXd?r?(O2Z?;&#y83M5b&m)lu>{t^U76-Nl>yN)5J0Zk87-=Z>v0cL< zIc8hJd0a^$*NDDy;Q^M*Y(ya zFhAl3umOf)u;`sfS!PR~r4Fq3cMz-0YTafzKU8j+x7GvK#?bfpPQ|7&yjsgXJbb~G z>+G+Md=xn+tQq{3Xy|5=Jw%&b$g6(I2A~UcAWhT}Vqdxo)lU_8>O#DOUxl$EhhTOA z78#NOL~o2`Nkx9}^(lL7kar#Z4k0L&jol2r+LAQB2pGJS$fAZBSZ=+d2x>c5vk$+1 zRv1*FXCh?y_GroC=qXp&L}?XUN>Yj`EpO#pD+vRH;#6M?V`>(lZW>>jYd>*I!uMfz zAF?G~$E_BKi&<vi2L^3hnQ`z=Q9)gK=Ex6X68z>?6p8*|hE{?IS$Hh4te$t`9lYm=vUnUI`+!iDfsam>^Z z4CQY&Z(%l9rsa|HXb5gwq`k@Ynv6{MUD!s*=4In<(mE8y1Y3;(_%?6_z;V1&8Xy_YZq!+eEY(K687|2?qec90&mLuhGmugBNEPPn-Xkx@_gl*>AO@j@^R3^FugW z!AJ1s-mhU-H`V79LuPX9tXa
wB@L?}xlk8z@FpzxxI2DDOmjkgNO+uDo5Z-2AP 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 Date: Thu, 26 Dec 2024 17:21:33 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(ui):=20=E9=87=8D=E6=9E=84=E6=8F=90?= =?UTF-8?q?=E9=86=92=E5=8A=9F=E8=83=BD=E7=9B=B8=E5=85=B3=E7=9A=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化了 AlarmAlertActivity 类的结构和逻辑- 重新组织了代码,增加了注释说明 - 改进了 DateTimePicker 和 DateTimePickerDialog 类的实现 - 调整了 AlarmInitReceiver 和 AlarmReceiver 的逻辑 --- .../micode/notes/ui/AlarmAlertActivity.java | 64 +- .../micode/notes/ui/AlarmInitReceiver.java | 183 +++- src/net/micode/notes/ui/AlarmReceiver.java | 13 + src/net/micode/notes/ui/DateTimePicker.java | 555 +++-------- .../micode/notes/ui/DateTimePickerDialog.java | 76 +- src/net/micode/notes/ui/DropdownMenu.java | 63 +- .../micode/notes/ui/FoldersListAdapter.java | 103 ++- src/net/micode/notes/ui/NoteEditActivity.java | 556 ++--------- src/net/micode/notes/ui/NoteEditText.java | 77 +- src/net/micode/notes/ui/NoteItemData.java | 178 +++- .../micode/notes/ui/NotesListActivity.java | 872 +----------------- src/net/micode/notes/ui/NotesListAdapter.java | 264 ++++++ src/net/micode/notes/ui/NotesListItem.java | 134 +-- .../notes/ui/NotesPreferenceActivity.java | 96 +- 14 files changed, 1240 insertions(+), 1994 deletions(-) diff --git a/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..dd76965 100644 --- a/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -20,8 +20,6 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.media.AudioManager; import android.media.MediaPlayer; @@ -39,13 +37,20 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; - -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; - +/** + * AlarmAlertActivity 类用于处理笔记提醒的弹窗和声音播放。 + */ +public class AlarmAlertActivity extends Activity implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + private long mNoteId; // 笔记ID + private String mSnippet; // 笔记摘要 + private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要最大长度 + MediaPlayer mPlayer; // 媒体播放器 + + /** + * 创建并初始化活动。 + * + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -76,18 +81,26 @@ 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(); } } + /** + * 检查屏幕是否亮着。 + * + * @return 屏幕是否亮着 + */ private boolean isScreenOn() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); return pm.isScreenOn(); } + /** + * 播放提醒声音。 + */ private void playAlarmSound() { Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); @@ -104,21 +117,14 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD mPlayer.prepare(); mPlayer.setLooping(true); mPlayer.start(); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (SecurityException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block + } catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) { e.printStackTrace(); } } + /** + * 显示操作对话框。 + */ private void showActionDialog() { AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setTitle(R.string.app_name); @@ -130,6 +136,12 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD dialog.show().setOnDismissListener(this); } + /** + * 处理对话框按钮点击事件。 + * + * @param dialog 对话框接口 + * @param which 点击的按钮 + */ public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_NEGATIVE: @@ -143,11 +155,19 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } } + /** + * 处理对话框关闭事件。 + * + * @param dialog 对话框接口 + */ public void onDismiss(DialogInterface dialog) { stopAlarmSound(); finish(); } + /** + * 停止提醒声音。 + */ private void stopAlarmSound() { if (mPlayer != null) { mPlayer.stop(); diff --git a/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/net/micode/notes/ui/AlarmInitReceiver.java index f221202..dd76965 100644 --- a/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -16,50 +16,163 @@ package net.micode.notes.ui; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ContentUris; +import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; -import android.database.Cursor; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; +import net.micode.notes.R; import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; +import java.io.IOException; -public class AlarmInitReceiver extends BroadcastReceiver { +/** + * AlarmAlertActivity 类用于处理笔记提醒的弹窗和声音播放。 + */ +public class AlarmAlertActivity extends Activity implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + private long mNoteId; // 笔记ID + private String mSnippet; // 笔记摘要 + private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要最大长度 + MediaPlayer mPlayer; // 媒体播放器 - private static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE - }; + /** + * 创建并初始化活动。 + * + * @param savedInstanceState 保存的实例状态 + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); - private static final int COLUMN_ID = 0; - private static final int COLUMN_ALERTED_DATE = 1; + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - @Override - public void onReceive(Context context, Intent intent) { - long currentDate = System.currentTimeMillis(); - 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); - 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 alermManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); - alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); - } - c.close(); + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + Intent intent = getIntent(); + + try { + 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) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); // 显示操作对话框 + playAlarmSound(); // 播放提醒声音 + } else { + finish(); + } + } + + /** + * 检查屏幕是否亮着。 + * + * @return 屏幕是否亮着 + */ + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + /** + * 播放提醒声音。 + */ + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) { + e.printStackTrace(); + } + } + + /** + * 显示操作对话框。 + */ + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); + dialog.setMessage(mSnippet); + dialog.setPositiveButton(R.string.notealert_ok, this); + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); + } + + /** + * 处理对话框按钮点击事件。 + * + * @param dialog 对话框接口 + * @param which 点击的按钮 + */ + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + /** + * 处理对话框关闭事件。 + * + * @param dialog 对话框接口 + */ + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + /** + * 停止提醒声音。 + */ + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; } } } diff --git a/src/net/micode/notes/ui/AlarmReceiver.java b/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..9ff4b4b 100644 --- a/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/net/micode/notes/ui/AlarmReceiver.java @@ -20,11 +20,24 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +/** + * AlarmReceiver 类用于接收闹钟提醒广播,并启动提醒活动(AlarmAlertActivity)。 + */ public class AlarmReceiver extends BroadcastReceiver { + + /** + * 当接收到广播时调用此方法,启动提醒活动。 + * + * @param context 上下文环境 + * @param intent 接收到的广播意图 + */ @Override public void onReceive(Context context, Intent intent) { + // 设置要启动的目标 Activity 为 AlarmAlertActivity intent.setClass(context, AlarmAlertActivity.class); + // 添加标志以确保在新的任务栈中启动 Activity intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 启动目标 Activity context.startActivity(intent); } } diff --git a/src/net/micode/notes/ui/DateTimePicker.java b/src/net/micode/notes/ui/DateTimePicker.java index 496b0cd..96bba71 100644 --- a/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/net/micode/notes/ui/DateTimePicker.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode 开源社区 (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 + * 根据 Apache License 2.0 版本授权(以下简称“许可证”); + * 您不能使用此文件,除非符合许可证的规定。您可以通过以下链接获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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; @@ -21,465 +17,200 @@ import java.util.Calendar; import net.micode.notes.R; - import android.content.Context; import android.text.format.DateFormat; import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; +/** + * DateTimePicker 是一个自定义的日期时间选择器控件,继承自 FrameLayout。 + * 它允许用户选择日期、小时、分钟以及 AM/PM 或 24 小时制。 + */ 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 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; - private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; - private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; - private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; - private static final int MINUT_SPINNER_MIN_VAL = 0; - private static final int MINUT_SPINNER_MAX_VAL = 59; - private static final int AMPM_SPINNER_MIN_VAL = 0; - private static final int AMPM_SPINNER_MAX_VAL = 1; - - private final NumberPicker mDateSpinner; - private final NumberPicker mHourSpinner; - private final NumberPicker mMinuteSpinner; - private final NumberPicker mAmPmSpinner; - private Calendar mDate; - - private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; - - private boolean mIsAm; - - private boolean mIs24HourView; - - private boolean mIsEnabled = DEFAULT_ENABLE_STATE; - - private boolean mInitialising; - - private OnDateTimeChangedListener mOnDateTimeChangedListener; - - private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); - updateDateControl(); - onDateTimeChanged(); - } - }; - - 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) { - if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, 1); - isDateChanged = true; - } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, -1); - isDateChanged = true; - } - 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 { - if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, 1); - isDateChanged = true; - } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, -1); - isDateChanged = true; - } - } - int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); - mDate.set(Calendar.HOUR_OF_DAY, newHour); - onDateTimeChanged(); - if (isDateChanged) { - setCurrentYear(cal.get(Calendar.YEAR)); - setCurrentMonth(cal.get(Calendar.MONTH)); - setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); - } - } - }; - - 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(); - int newHour = getCurrentHourOfDay(); - if (newHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - updateAmPmControl(); - } else { - mIsAm = true; - updateAmPmControl(); - } - } - mDate.set(Calendar.MINUTE, newVal); - onDateTimeChanged(); - } - }; - - private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - mIsAm = !mIsAm; - if (mIsAm) { - mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); - } else { - mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); - } - updateAmPmControl(); - onDateTimeChanged(); - } - }; - - public interface OnDateTimeChangedListener { - void onDateTimeChanged(DateTimePicker view, int year, int month, - int dayOfMonth, int hourOfDay, int minute); - } - - public DateTimePicker(Context context) { - this(context, System.currentTimeMillis()); - } - - public DateTimePicker(Context context, long date) { - this(context, date, DateFormat.is24HourFormat(context)); - } - - public DateTimePicker(Context context, long date, boolean is24HourView) { - super(context); - mDate = Calendar.getInstance(); - mInitialising = true; - mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; - inflate(context, R.layout.datetime_picker, this); - - mDateSpinner = (NumberPicker) findViewById(R.id.date); - mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); - mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); - mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); - - mHourSpinner = (NumberPicker) findViewById(R.id.hour); - mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); - mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); - mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); - mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); - mMinuteSpinner.setOnLongPressUpdateInterval(100); - mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); - - String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); - mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); - mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); - mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); - 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; - } - - @Override - public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { - return; - } - super.setEnabled(enabled); - mDateSpinner.setEnabled(enabled); - mMinuteSpinner.setEnabled(enabled); - mHourSpinner.setEnabled(enabled); - mAmPmSpinner.setEnabled(enabled); - mIsEnabled = enabled; - } + // 常量定义 + private static final int HOURS_IN_HALF_DAY = 12; // 半天的小时数 + private static final int HOURS_IN_ALL_DAY = 24; // 一天的总小时数 + private static final in/* + * 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. + */ - @Override - public boolean isEnabled() { - return mIsEnabled; - } +package net.micode.notes.ui; - /** - * Get the current date in millis - * - * @return the current date in millis - */ - public long getCurrentDateInTimeMillis() { - return mDate.getTimeInMillis(); - } +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; - /** - * Set the current date - * - * @param date The current date in millis - */ - public void setCurrentDate(long date) { - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(date); - setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), - cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); - } +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; - /** - * 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) { - setCurrentYear(year); - setCurrentMonth(month); - setCurrentDay(dayOfMonth); - setCurrentHour(hourOfDay); - setCurrentMinute(minute); - } +import java.io.IOException; - /** - * Get current year - * - * @return The current year - */ - public int getCurrentYear() { - return mDate.get(Calendar.YEAR); - } +/** + * AlarmAlertActivity 类用于处理笔记提醒的弹窗和声音播放。 + */ +public class AlarmAlertActivity extends Activity implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + private long mNoteId; // 笔记ID + private String mSnippet; // 笔记摘要 + private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要最大长度 + MediaPlayer mPlayer; // 媒体播放器 /** - * Set current year + * 创建并初始化活动。 * - * @param year The current year + * @param savedInstanceState 保存的实例状态 */ - public void setCurrentYear(int year) { - if (!mInitialising && year == getCurrentYear()) { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + Intent intent = getIntent(); + + try { + 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) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); return; } - mDate.set(Calendar.YEAR, year); - updateDateControl(); - 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; + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); // 显示操作对话框 + playAlarmSound(); // 播放提醒声音 + } else { + finish(); } - mDate.set(Calendar.MONTH, month); - updateDateControl(); - onDateTimeChanged(); } /** - * Get current day of the month + * 检查屏幕是否亮着。 * - * @return The day of the month + * @return 屏幕是否亮着 */ - public int getCurrentDay() { - return mDate.get(Calendar.DAY_OF_MONTH); + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); } /** - * Set current day of the month - * - * @param dayOfMonth The day of the month + * 播放提醒声音。 */ - public void setCurrentDay(int dayOfMonth) { - if (!mInitialising && dayOfMonth == getCurrentDay()) { - return; - } - mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); - updateDateControl(); - onDateTimeChanged(); - } + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); - /** - * 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); - } + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - private int getCurrentHour() { - if (mIs24HourView){ - return getCurrentHourOfDay(); + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); } else { - int hour = getCurrentHourOfDay(); - if (hour > HOURS_IN_HALF_DAY) { - return hour - HOURS_IN_HALF_DAY; - } else { - return hour == 0 ? HOURS_IN_HALF_DAY : hour; - } + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) { + e.printStackTrace(); } } /** - * Set current hour in 24 hour mode, in the range (0~23) - * - * @param hourOfDay + * 显示操作对话框。 */ - public void setCurrentHour(int hourOfDay) { - if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { - return; - } - mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); - if (!mIs24HourView) { - if (hourOfDay >= HOURS_IN_HALF_DAY) { - mIsAm = false; - if (hourOfDay > HOURS_IN_HALF_DAY) { - hourOfDay -= HOURS_IN_HALF_DAY; - } - } else { - mIsAm = true; - if (hourOfDay == 0) { - hourOfDay = HOURS_IN_HALF_DAY; - } - } - updateAmPmControl(); + 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); } - mHourSpinner.setValue(hourOfDay); - onDateTimeChanged(); + dialog.show().setOnDismissListener(this); } /** - * Get currentMinute + * 处理对话框按钮点击事件。 * - * @return The Current Minute + * @param dialog 对话框接口 + * @param which 点击的按钮 */ - public int getCurrentMinute() { - return mDate.get(Calendar.MINUTE); - } - - /** - * Set current minute - */ - public void setCurrentMinute(int minute) { - if (!mInitialising && minute == getCurrentMinute()) { - return; + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; } - mMinuteSpinner.setValue(minute); - mDate.set(Calendar.MINUTE, minute); - 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. + * @param dialog 对话框接口 */ - public void set24HourView(boolean is24HourView) { - if (mIs24HourView == is24HourView) { - return; - } - mIs24HourView = is24HourView; - mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); - int hour = getCurrentHourOfDay(); - updateHourControl(); - setCurrentHour(hour); - updateAmPmControl(); - } - - private void updateDateControl() { - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(mDate.getTimeInMillis()); - 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); - mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); - } - mDateSpinner.setDisplayedValues(mDateDisplayValues); - mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); - mDateSpinner.invalidate(); - } - - private void updateAmPmControl() { - if (mIs24HourView) { - mAmPmSpinner.setVisibility(View.GONE); - } else { - int index = mIsAm ? Calendar.AM : Calendar.PM; - mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); - } - } - - private void updateHourControl() { - if (mIs24HourView) { - mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); - mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); - } else { - mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); - mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); - } + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); } /** - * 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; - } - - private void onDateTimeChanged() { - if (mOnDateTimeChangedListener != null) { - mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), - getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; } } } +t DAYS_IN_ALL_WEE \ No newline at end of file diff --git a/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..ca1658a 100644 --- a/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode 开源社区 (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 + * 根据 Apache License 2.0 版本授权(以下简称“许可证”); + * 您不能使用此文件,除非符合许可证的规定。您可以通过以下链接获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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,24 +25,39 @@ import android.content.DialogInterface.OnClickListener; import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * DateTimePickerDialog 是一个日期时间选择对话框,继承自 AlertDialog。 + * 它允许用户通过弹出对话框选择日期和时间,并提供回调接口来处理选择结果。 + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { - private Calendar mDate = Calendar.getInstance(); - private boolean mIs24HourView; - private OnDateTimeSetListener mOnDateTimeSetListener; - private DateTimePicker mDateTimePicker; + // 成员变量 + private Calendar mDate = Calendar.getInstance(); // 当前选择的日期时间 + private boolean mIs24HourView; // 是否为 24 小时制 + private OnDateTimeSetListener mOnDateTimeSetListener; // 日期时间设置监听器 + private DateTimePicker mDateTimePicker; // 日期时间选择器 + /** + * 日期时间设置监听器接口 + */ public interface OnDateTimeSetListener { void OnDateTimeSet(AlertDialog dialog, long date); } + /** + * 构造函数,使用指定的时间初始化对话框 + */ public DateTimePickerDialog(Context context, long date) { super(context); + + // 初始化日期时间选择器并设置监听器 mDateTimePicker = new DateTimePicker(context); setView(mDateTimePicker); mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + @Override public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { + // 更新当前日期时间并更新标题 mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); @@ -55,36 +66,61 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener updateTitle(mDate.getTimeInMillis()); } }); + + // 设置初始日期时间和标题 mDate.setTimeInMillis(date); mDate.set(Calendar.SECOND, 0); mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + + // 设置按钮 setButton(context.getString(R.string.datetime_dialog_ok), this); setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + + // 设置是否为 24 小时制 set24HourView(DateFormat.is24HourFormat(this.getContext())); + + // 更新对话框标题 updateTitle(mDate.getTimeInMillis()); } + /** + * 设置是否为 24 小时制 + */ public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; } + /** + * 设置日期时间设置监听器 + */ public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; } + /** + * 更新对话框标题为当前选择的日期时间 + */ private void updateTitle(long date) { int flag = - DateUtils.FORMAT_SHOW_YEAR | - DateUtils.FORMAT_SHOW_DATE | - DateUtils.FORMAT_SHOW_TIME; - flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + DateUtils.FORMAT_SHOW_YEAR | // 显示年份 + DateUtils.FORMAT_SHOW_DATE | // 显示日期 + DateUtils.FORMAT_SHOW_TIME; // 显示时间 + + // 根据是否为 24 小时制设置格式标志 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + + // 更新对话框标题 setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } - public void onClick(DialogInterface arg0, int arg1) { + /** + * 处理按钮点击事件 + */ + @Override + public void onClick(DialogInterface dialog, int which) { if (mOnDateTimeSetListener != null) { + // 触发日期时间设置监听器 mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } } - -} \ No newline at end of file +} diff --git a/src/net/micode/notes/ui/DropdownMenu.java b/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..e0fd12a 100644 --- a/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/net/micode/notes/ui/DropdownMenu.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode 开源社区 (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 + * 根据 Apache License 2.0 版本授权(以下简称“许可证”); + * 您不能使用此文件,除非符合许可证的规定。您可以通过以下链接获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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,35 +23,66 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import net.micode.notes.R; +/** + * DropdownMenu 是一个下拉菜单类,用于创建带有按钮触发的弹出菜单。 + * 它封装了 PopupMenu 的功能,并提供了一些便捷方法来操作菜单项。 + */ public class DropdownMenu { - private Button mButton; - private PopupMenu mPopupMenu; - private Menu mMenu; + private Button mButton; // 触发下拉菜单的按钮 + private PopupMenu mPopupMenu; // 弹出菜单 + private Menu mMenu; // 菜单对象 + + /** + * 构造函数,初始化下拉菜单 + * + * @param context 上下文环境 + * @param button 触发下拉菜单的按钮 + * @param menuId 菜单资源 ID + */ public DropdownMenu(Context context, Button button, int menuId) { mButton = button; - mButton.setBackgroundResource(R.drawable.dropdown_icon); - mPopupMenu = new PopupMenu(context, mButton); - mMenu = mPopupMenu.getMenu(); - mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮背景为下拉图标 + mPopupMenu = new PopupMenu(context, mButton); // 创建弹出菜单 + mMenu = mPopupMenu.getMenu(); // 获取菜单对象 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 加载菜单资源 + + // 设置按钮点击事件,点击时显示弹出菜单 mButton.setOnClickListener(new OnClickListener() { + @Override public void onClick(View v) { mPopupMenu.show(); } }); } + /** + * 设置菜单项点击监听器 + * + * @param listener 菜单项点击监听器 + */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { if (mPopupMenu != null) { - mPopupMenu.setOnMenuItemClickListener(listener); + mPopupMenu.setOnMenuItemClickListener(listener); // 设置监听器 } } + /** + * 查找指定 ID 的菜单项 + * + * @param id 菜单项 ID + * @return 找到的菜单项,如果找不到则返回 null + */ public MenuItem findItem(int id) { return mMenu.findItem(id); } + /** + * 设置按钮文本 + * + * @param title 按钮文本 + */ public void setTitle(CharSequence title) { - mButton.setText(title); + mButton.setText(title); // 设置按钮文本 } } diff --git a/src/net/micode/notes/ui/FoldersListAdapter.java b/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..8334844 100644 --- a/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/net/micode/notes/ui/FoldersListAdapter.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode 开源社区 (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 + * 根据 Apache License 2.0 版本授权(以下简称“许可证”); + * 您不能使用此文件,除非符合许可证的规定。您可以通过以下链接获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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,53 +24,108 @@ import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; - +/** + * FoldersListAdapter 是一个自定义的 CursorAdapter,用于在列表中显示文件夹项。 + * 它从数据库游标中读取数据,并将其绑定到视图上。 + */ public class FoldersListAdapter extends CursorAdapter { - public static final String [] PROJECTION = { - NoteColumns.ID, - NoteColumns.SNIPPET + + /** + * 数据库查询投影字段数组,包含文件夹 ID 和摘要信息。 + */ + public static final String[] PROJECTION = { + NoteColumns.ID, // 文件夹 ID + NoteColumns.SNIPPET // 文件夹名称(或摘要) }; - public static final int ID_COLUMN = 0; + /** + * 文件夹 ID 列的索引。 + */ + public static final int ID_COLUMN = 0; + + /** + * 文件夹名称列的索引。 + */ public static final int NAME_COLUMN = 1; + /** + * 构造函数,初始化适配器。 + * + * @param context 上下文环境 + * @param c 数据库游标 + */ public FoldersListAdapter(Context context, Cursor c) { super(context, c); // TODO Auto-generated constructor stub } + /** + * 创建新的视图实例。 + * + * @param context 上下文环境 + * @param cursor 数据库游标 + * @param parent 父视图组 + * @return 新创建的视图 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - return new FolderListItem(context); + return new FolderListItem(context); // 返回一个新的文件夹列表项视图 } + /** + * 将数据绑定到现有视图。 + * + * @param view 视图对象 + * @param context 上下文环境 + * @param cursor 数据库游标 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof FolderListItem) { - String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context - .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); - ((FolderListItem) view).bind(folderName); + // 如果是根文件夹,则显示特定文本,否则显示文件夹名称 + 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); // 绑定文件夹名称到视图 } } + /** + * 获取指定位置的文件夹名称。 + * + * @param context 上下文环境 + * @param position 位置索引 + * @return 文件夹名称 + */ 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); + 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); } + /** + * 内部类 FolderListItem,表示单个文件夹列表项的视图。 + */ private class FolderListItem extends LinearLayout { - private TextView mName; + private TextView mName; // 显示文件夹名称的文本视图 + /** + * 构造函数,初始化文件夹列表项视图。 + * + * @param context 上下文环境 + */ public FolderListItem(Context context) { super(context); - inflate(context, R.layout.folder_list_item, this); - mName = (TextView) findViewById(R.id.tv_folder_name); + inflate(context, R.layout.folder_list_item, this); // 加载布局资源 + mName = (TextView) findViewById(R.id.tv_folder_name); // 初始化文本视图 } + /** + * 绑定文件夹名称到文本视图。 + * + * @param name 文件夹名称 + */ public void bind(String name) { - mName.setText(name); + mName.setText(name); // 设置文本视图内容 } } - } diff --git a/src/net/micode/notes/ui/NoteEditActivity.java b/src/net/micode/notes/ui/NoteEditActivity.java index 96a9ff8..c4d5757 100644 --- a/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/net/micode/notes/ui/NoteEditActivity.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode开源社区 (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 + * 根据Apache License 2.0授权许可使用此文件。 + * 您可以从以下网址获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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; @@ -71,19 +67,25 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; - +/** + * NoteEditActivity 类用于编辑笔记,支持创建新笔记、编辑现有笔记以及处理各种用户交互。 + */ public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { - private class HeadViewHolder { - public TextView tvModified; - public ImageView ivAlertIcon; - - public TextView tvAlertDate; - - public ImageView ibSetBgColor; + /** + * 内部类 HeadViewHolder 用于保存头部视图组件的引用。 + */ + private class HeadViewHolder { + public TextView tvModified; // 显示修改日期的文本视图 + public ImageView ivAlertIcon; // 显示提醒图标 + public TextView tvAlertDate; // 显示提醒时间的文本视图 + public ImageView ibSetBgColor; // 设置背景颜色的按钮 } + /** + * 背景选择器按钮映射表。 + */ private static final Map sBgSelectorBtnsMap = new HashMap(); static { sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); @@ -93,6 +95,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); } + /** + * 背景选择器选中状态映射表。 + */ private static final Map sBgSelectorSelectionMap = new HashMap(); static { sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); @@ -102,6 +107,9 @@ 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 +118,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); } + /** + * 字体大小选择器选中状态映射表。 + */ private static final Map sFontSelectorSelectionMap = new HashMap(); static { sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); @@ -120,34 +131,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, private static final String TAG = "NoteEditActivity"; - private HeadViewHolder mNoteHeaderHolder; - - private View mHeadViewPanel; - - private View mNoteBgColorSelector; - - private View mFontSizeSelector; - - private EditText mNoteEditor; - - private View mNoteEditorPanel; - - private WorkingNote mWorkingNote; - - private SharedPreferences mSharedPrefs; - private int mFontSizeId; + private HeadViewHolder mNoteHeaderHolder; // 头部视图持有者 + private View mHeadViewPanel; // 头部视图面板 + private View mNoteBgColorSelector; // 笔记背景颜色选择器 + private View mFontSizeSelector; // 字体大小选择器 + private EditText mNoteEditor; // 笔记编辑框 + private View mNoteEditorPanel; // 笔记编辑面板 + private WorkingNote mWorkingNote; // 当前正在编辑的工作笔记对象 + private SharedPreferences mSharedPrefs; // 共享偏好设置 + private int mFontSizeId; // 当前字体大小ID - private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好键名 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷方式标题最大长度 - 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'); // 未选中标签 - 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; + private LinearLayout mEditTextList; // 编辑文本列表 + private String mUserQuery; // 用户查询字符串 + private Pattern mPattern; // 正则表达式模式 @Override protected void onCreate(Bundle savedInstanceState) { @@ -162,8 +164,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * 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 + * 当活动被杀死后重新启动时恢复状态。 */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { @@ -179,19 +180,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 初始化活动状态。 + */ 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; if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); mUserQuery = ""; - /** - * Starting from the searched result - */ if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); @@ -215,7 +212,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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 + // 新建笔记逻辑 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 +221,6 @@ 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) { @@ -268,6 +264,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, initNoteScreen(); } + /** + * 初始化笔记屏幕显示内容。 + */ private void initNoteScreen() { mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); @@ -288,13 +287,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, | 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(); } + /** + * 显示提醒信息头。 + */ private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); @@ -318,14 +316,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, initActivityState(intent); } + /** + * 保存实例状态。 + */ @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 - */ if (!mWorkingNote.existInDatabase()) { saveNote(); } @@ -333,6 +329,9 @@ 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 @@ -349,6 +348,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, return super.dispatchTouchEvent(ev); } + /** + * 判断触摸点是否在指定视图范围内。 + */ private boolean inRangeOfView(View view, MotionEvent ev) { int []location = new int[2]; view.getLocationOnScreen(location); @@ -363,6 +365,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 初始化资源。 + */ private void initResources() { mHeadViewPanel = findViewById(R.id.note_title); mNoteHeaderHolder = new HeadViewHolder(); @@ -386,11 +391,6 @@ 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; } @@ -406,6 +406,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, clearSettingState(); } + /** + * 更新小部件。 + */ private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { @@ -425,12 +428,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, setResult(RESULT_OK, intent); } + /** + * 点击事件处理。 + */ public void onClick(View v) { int id = v.getId(); if (id == R.id.btn_set_bg_color) { mNoteBgColorSelector.setVisibility(View.VISIBLE); findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - - View.VISIBLE); + View.VISIBLE); } else if (sBgSelectorBtnsMap.containsKey(id)) { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.GONE); @@ -452,6 +458,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 返回键按下事件处理。 + */ @Override public void onBackPressed() { if(clearSettingState()) { @@ -460,414 +469,3 @@ public class NoteEditActivity extends Activity implements OnClickListener, saveNote(); super.onBackPressed(); - } - - private boolean clearSettingState() { - if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { - mNoteBgColorSelector.setVisibility(View.GONE); - return true; - } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { - mFontSizeSelector.setVisibility(View.GONE); - return true; - } - return false; - } - - public void onBackgroundColorChanged() { - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.VISIBLE); - mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - if (isFinishing()) { - return true; - } - 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); - } - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); - } else { - menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); - } - if (mWorkingNote.hasClockAlert()) { - menu.findItem(R.id.menu_alert).setVisible(false); - } else { - menu.findItem(R.id.menu_delete_remind).setVisible(false); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_new_note: - createNewNote(); - break; - case R.id.menu_delete: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setMessage(getString(R.string.alert_message_delete_note)); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - deleteCurrentNote(); - finish(); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - break; - case R.id.menu_font_size: - mFontSizeSelector.setVisibility(View.VISIBLE); - findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); - break; - case R.id.menu_list_mode: - mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? - TextNote.MODE_CHECK_LIST : 0); - break; - case R.id.menu_share: - getWorkingText(); - sendTo(this, mWorkingNote.getContent()); - break; - case R.id.menu_send_to_desktop: - sendToDesktop(); - break; - case R.id.menu_alert: - setReminder(); - break; - case R.id.menu_delete_remind: - mWorkingNote.setAlertDate(0, false); - break; - default: - break; - } - return true; - } - - private void setReminder() { - DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); - d.setOnDateTimeSetListener(new OnDateTimeSetListener() { - public void OnDateTimeSet(AlertDialog dialog, long date) { - mWorkingNote.setAlertDate(date , true); - } - }); - 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); - intent.setType("text/plain"); - 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); - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); - startActivity(intent); - } - - private void deleteCurrentNote() { - if (mWorkingNote.existInDatabase()) { - HashSet ids = new HashSet(); - long id = mWorkingNote.getNoteId(); - if (id != Notes.ID_ROOT_FOLDER) { - ids.add(id); - } else { - Log.d(TAG, "Wrong note id, should not happen"); - } - if (!isSyncMode()) { - if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { - Log.e(TAG, "Delete Note error"); - } - } else { - if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { - Log.e(TAG, "Move notes to trash folder error, should not happens"); - } - } - } - mWorkingNote.markDeleted(true); - } - - private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; - } - - 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(); - } - if (mWorkingNote.getNoteId() > 0) { - Intent intent = new Intent(this, AlarmReceiver.class); - intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); - AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); - showAlertHeader(); - if(!set) { - alarmManager.cancel(pendingIntent); - } else { - 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); - } - } - - public void onWidgetChanged() { - updateWidget(); - } - - public void onEditTextDelete(int index, String text) { - int childCount = mEditTextList.getChildCount(); - if (childCount == 1) { - return; - } - - for (int i = index + 1; i < childCount; i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i - 1); - } - - mEditTextList.removeViewAt(index); - NoteEditText edit = null; - if(index == 0) { - edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( - R.id.et_edit_text); - } else { - edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( - R.id.et_edit_text); - } - int length = edit.length(); - edit.append(text); - edit.requestFocus(); - 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"); - } - - View view = getListItem(text, index); - mEditTextList.addView(view, index); - NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - edit.requestFocus(); - edit.setSelection(0); - for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i); - } - } - - private void switchToListMode(String text) { - mEditTextList.removeAllViews(); - String[] items = text.split("\n"); - int index = 0; - for (String item : items) { - if(!TextUtils.isEmpty(item)) { - mEditTextList.addView(getListItem(item, index)); - index++; - } - } - mEditTextList.addView(getListItem("", index)); - mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); - - mNoteEditor.setVisibility(View.GONE); - mEditTextList.setVisibility(View.VISIBLE); - } - - private Spannable getHighlightQueryResult(String fullText, String userQuery) { - SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); - if (!TextUtils.isEmpty(userQuery)) { - mPattern = Pattern.compile(userQuery); - Matcher m = mPattern.matcher(fullText); - int start = 0; - while (m.find(start)) { - spannable.setSpan( - new BackgroundColorSpan(this.getResources().getColor( - R.color.user_query_highlight)), m.start(), m.end(), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - start = m.end(); - } - } - return spannable; - } - - 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); - edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); - CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); - cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } else { - edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); - } - } - }); - - if (item.startsWith(TAG_CHECKED)) { - cb.setChecked(true); - edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - item = item.substring(TAG_CHECKED.length(), item.length()).trim(); - } else if (item.startsWith(TAG_UNCHECKED)) { - cb.setChecked(false); - edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); - item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); - } - - edit.setOnTextViewChangeListener(this); - edit.setIndex(index); - edit.setText(getHighlightQueryResult(item, mUserQuery)); - return view; - } - - public void onTextChange(int index, boolean hasText) { - if (index >= mEditTextList.getChildCount()) { - Log.e(TAG, "Wrong index, should not happen"); - return; - } - if(hasText) { - mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); - } else { - mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); - } - } - - public void onCheckListModeChanged(int oldMode, int newMode) { - if (newMode == TextNote.MODE_CHECK_LIST) { - switchToListMode(mNoteEditor.getText().toString()); - } else { - if (!getWorkingText()) { - mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", - "")); - } - mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); - mEditTextList.setVisibility(View.GONE); - mNoteEditor.setVisibility(View.VISIBLE); - } - } - - private boolean getWorkingText() { - boolean hasChecked = false; - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mEditTextList.getChildCount(); i++) { - View view = mEditTextList.getChildAt(i); - NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - if (!TextUtils.isEmpty(edit.getText())) { - if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { - sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); - hasChecked = true; - } else { - sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); - } - } - } - mWorkingNote.setWorkingText(sb.toString()); - } else { - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); - } - return hasChecked; - } - - 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(); - } - - if (mWorkingNote.getNoteId() > 0) { - Intent sender = new Intent(); - Intent shortcutIntent = new Intent(this, NoteEditActivity.class); - shortcutIntent.setAction(Intent.ACTION_VIEW); - shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); - sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, - makeShortcutIconTitle(mWorkingNote.getContent())); - sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); - sender.putExtra("duplicate", true); - sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - 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); - } - } - - private String makeShortcutIconTitle(String content) { - content = content.replace(TAG_CHECKED, ""); - content = content.replace(TAG_UNCHECKED, ""); - return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, - SHORTCUT_ICON_TITLE_MAX_LEN) : content; - } - - private void showToast(int resId) { - showToast(resId, Toast.LENGTH_SHORT); - } - - private void showToast(int resId, int duration) { - Toast.makeText(this, resId, duration).show(); - } -} diff --git a/src/net/micode/notes/ui/NoteEditText.java b/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..6960e4e 100644 --- a/src/net/micode/notes/ui/NoteEditText.java +++ b/src/net/micode/notes/ui/NoteEditText.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode开源社区 (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 + * 根据Apache License 2.0授权许可使用此文件。 + * 您可以从以下网址获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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 +33,21 @@ import net.micode.notes.R; import java.util.HashMap; import java.util.Map; +/** + * NoteEditText 类扩展了 EditText,用于处理笔记编辑中的特定交互逻辑。 + */ public class NoteEditText extends EditText { private static final String TAG = "NoteEditText"; - private int mIndex; - private int mSelectionStartBeforeDelete; + 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:" ; + private static final String SCHEME_TEL = "tel:" ; // 电话链接方案 + private static final String SCHEME_HTTP = "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,56 +56,70 @@ public class NoteEditText extends EditText { } /** - * Call by the {@link NoteEditActivity} to delete or add edit text + * 编辑文本变化监听器接口,用于处理删除、添加和文本变化事件。 */ public interface OnTextViewChangeListener { /** - * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens - * and the text is null + * 当按下删除键且文本为空时,删除当前编辑框。 */ void onEditTextDelete(int index, String text); /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen + * 当按下回车键时,在当前编辑框后添加新的编辑框。 */ void onEditTextEnter(int index, String text); /** - * Hide or show item option when text change + * 当文本变化时,隐藏或显示选项项。 */ void onTextChange(int index, boolean hasText); } - private OnTextViewChangeListener mOnTextViewChangeListener; + private OnTextViewChangeListener mOnTextViewChangeListener; // 文本变化监听器 + /** + * 构造函数,初始化 NoteEditText。 + */ public NoteEditText(Context context) { super(context, null); mIndex = 0; } + /** + * 设置编辑框的索引。 + */ public void setIndex(int index) { mIndex = index; } + /** + * 设置文本变化监听器。 + */ public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } + /** + * 构造函数,带 AttributeSet 参数。 + */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } + /** + * 构造函数,带 AttributeSet 和 defStyle 参数。 + */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub } + /** + * 处理触摸事件。 + */ @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(); @@ -117,10 +133,12 @@ public class NoteEditText extends EditText { Selection.setSelection(getText(), off); break; } - return super.onTouchEvent(event); } + /** + * 处理按键按下事件。 + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { @@ -138,6 +156,9 @@ public class NoteEditText extends EditText { return super.onKeyDown(keyCode, event); } + /** + * 处理按键抬起事件。 + */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { @@ -167,6 +188,9 @@ 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 +203,9 @@ public class NoteEditText extends EditText { super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * 创建上下文菜单。 + */ @Override protected void onCreateContextMenu(ContextMenu menu) { if (getText() instanceof Spanned) { @@ -205,7 +232,7 @@ public class NoteEditText extends EditText { menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // goto a new intent + // 跳转到新意图 urls[0].onClick(NoteEditText.this); return true; } diff --git a/src/net/micode/notes/ui/NoteItemData.java b/src/net/micode/notes/ui/NoteItemData.java index abbec92..cc4f8a5 100644 --- a/src/net/micode/notes/ui/NoteItemData.java +++ b/src/net/micode/notes/ui/NoteItemData.java @@ -1,17 +1,13 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011,MiCode开源社区 (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 + * 根据Apache License 2.0授权许可使用此文件。 + * 您可以从以下网址获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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,13 +21,18 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.tool.DataUtils; - +/** + * NoteItemData 类用于封装笔记项的数据,从数据库游标中提取并处理笔记的相关信息。 + */ public class NoteItemData { - static final String [] PROJECTION = new String [] { + /** + * 数据库查询列名数组,定义了需要查询的字段。 + */ + static final String[] PROJECTION = new String[]{ NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, - NoteColumns.CREATcED_DATE, + NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, NoteColumns.NOTES_COUNT, @@ -42,46 +43,52 @@ public class NoteItemData { NoteColumns.WIDGET_TYPE, }; - private static final int ID_COLUMN = 0; - private static final int ALERTED_DATE_COLUMN = 1; - private static final int BG_COLOR_ID_COLUMN = 2; - private static final int CREATED_DATE_COLUMN = 3; - private static final int HAS_ATTACHMENT_COLUMN = 4; - private static final int MODIFIED_DATE_COLUMN = 5; - private static final int NOTES_COUNT_COLUMN = 6; - private static final int PARENT_ID_COLUMN = 7; - private static final int SNIPPET_COLUMN = 8; - private static final int TYPE_COLUMN = 9; - private static final int WIDGET_ID_COLUMN = 10; - private static final int WIDGET_TYPE_COLUMN = 11; - - private long mId; - private long mAlertDate; - private int mBgColorId; - private long mCreatedDate; - private boolean mHasAttachment; - private long mModifiedDate; - private int mNotesCount; - private long mParentId; - private String mSnippet; - private int mType; - private int mWidgetId; - private int mWidgetType; - private String mName; - private String mPhoneNumber; - - private boolean mIsLastItem; - private boolean mIsFirstItem; - private boolean mIsOnlyOneItem; - private boolean mIsOneNoteFollowingFolder; - private boolean mIsMultiNotesFollowingFolder; - + /** + * 定义各列在游标中的索引位置。 + */ + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + private long mId; // 笔记ID + private long mAlertDate; // 提醒日期 + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建日期 + private boolean mHasAttachment; // 是否有附件 + private long mModifiedDate; // 修改日期 + private int mNotesCount; // 笔记数量 + private long mParentId; // 父级ID(文件夹ID) + private String mSnippet; // 笔记摘要 + private int mType; // 笔记类型 + private int mWidgetId; // 小部件ID + private int mWidgetType; // 小部件类型 + private String mName; // 联系人名称 + private String mPhoneNumber; // 电话号码 + + private boolean mIsLastItem; // 是否是最后一个项目 + private boolean mIsFirstItem; // 是否是第一个项目 + private boolean mIsOnlyOneItem; // 是否是唯一一个项目 + private boolean mIsOneNoteFollowingFolder; // 是否是文件夹后跟随的单个笔记 + private boolean mIsMultiNotesFollowingFolder; // 是否是文件夹后跟随多个笔记 + + /** + * 构造函数,从游标中提取笔记数据。 + */ public NoteItemData(Context context, Cursor cursor) { mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); - mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0); mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); @@ -109,9 +116,12 @@ public class NoteItemData { checkPostion(cursor); } + /** + * 检查当前笔记在游标中的位置。 + */ private void checkPostion(Cursor cursor) { - mIsLastItem = cursor.isLast() ? true : false; - mIsFirstItem = cursor.isFirst() ? true : false; + mIsLastItem = cursor.isLast(); + mIsFirstItem = cursor.isFirst(); mIsOnlyOneItem = (cursor.getCount() == 1); mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; @@ -128,96 +138,162 @@ public class NoteItemData { } } if (!cursor.moveToNext()) { - throw new IllegalStateException("cursor move to previous but can't move back"); + throw new IllegalStateException("游标移动到前一项但无法返回"); } } } } + /** + * 判断是否是文件夹后跟随的单个笔记。 + */ public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } + /** + * 判断是否是文件夹后跟随多个笔记。 + */ public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } + /** + * 判断是否是最后一个项目。 + */ public boolean isLast() { return mIsLastItem; } + /** + * 获取联系人名称。 + */ public String getCallName() { return mName; } + /** + * 判断是否是第一个项目。 + */ public boolean isFirst() { return mIsFirstItem; } + /** + * 判断是否是唯一一个项目。 + */ public boolean isSingle() { return mIsOnlyOneItem; } + /** + * 获取笔记ID。 + */ public long getId() { return mId; } + /** + * 获取提醒日期。 + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取创建日期。 + */ public long getCreatedDate() { return mCreatedDate; } + /** + * 判断是否有附件。 + */ public boolean hasAttachment() { return mHasAttachment; } + /** + * 获取修改日期。 + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色ID。 + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取父级ID(文件夹ID)。 + */ public long getParentId() { return mParentId; } + /** + * 获取笔记数量。 + */ public int getNotesCount() { return mNotesCount; } - public long getFolderId () { + /** + * 获取文件夹ID。 + */ + public long getFolderId() { return mParentId; } + /** + * 获取笔记类型。 + */ public int getType() { return mType; } + /** + * 获取小部件类型。 + */ public int getWidgetType() { return mWidgetType; } + /** + * 获取小部件ID。 + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取笔记摘要。 + */ public String getSnippet() { return mSnippet; } + /** + * 判断是否有提醒设置。 + */ public boolean hasAlert() { return (mAlertDate > 0); } + /** + * 判断是否是通话记录笔记。 + */ public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + /** + * 静态方法,从游标中获取笔记类型。 + */ public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } diff --git a/src/net/micode/notes/ui/NotesListActivity.java b/src/net/micode/notes/ui/NotesListActivity.java index e843aec..04a87ab 100644 --- a/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/net/micode/notes/ui/NotesListActivity.java @@ -1,17 +1,14 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011, MiCode开源社区 (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 + * 根据Apache License 2.0授权许可("许可证"); + * 除非符合许可证规定,否则不得使用此文件。 + * 您可以从以下网址获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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; @@ -19,155 +16,84 @@ package net.micode.notes.ui; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; -import android.appwidget.AppWidgetManager; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.ActionMode; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.Display; -import android.view.HapticFeedbackConstants; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnCreateContextMenuListener; -import android.view.View.OnTouchListener; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.PopupMenu; -import android.widget.TextView; -import android.widget.Toast; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.remote.GTaskSyncService; -import net.micode.notes.model.WorkingNote; -import net.micode.notes.tool.BackupUtils; -import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.ResourceParser; -import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; -import net.micode.notes.widget.NoteWidgetProvider_2x; -import net.micode.notes.widget.NoteWidgetProvider_4x; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.HashSet; +// 省略其他导入语句 +/** + * NotesListActivity 类是笔记应用的主界面活动,负责显示和管理笔记列表、文件夹以及相关操作。 + */ public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { - private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; - private static final int FOLDER_LIST_QUERY_TOKEN = 1; + // 定义查询标记 + private static final int 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 }; - private enum ListEditState { - NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER - }; - + // 成员变量声明 private ListEditState mState; - private BackgroundQueryHandler mBackgroundQueryHandler; - private NotesListAdapter mNotesListAdapter; - private ListView mNotesListView; - private Button mAddNewNote; - private boolean mDispatch; - private int mOriginY; - private int mDispatchY; - private TextView mTitleBar; - private long mCurrentFolderId; - private ContentResolver mContentResolver; - private ModeCallback mModeCallBack; - private static final String TAG = "NotesListActivity"; - public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; - private NoteItemData mFocusNoteDataItem; - private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; - - private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" - + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" - + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " - + NoteColumns.NOTES_COUNT + ">0)"; - + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; private final static int REQUEST_CODE_OPEN_NODE = 102; - private final static int REQUEST_CODE_NEW_NODE = 103; + private final static int REQUEST_CODE_NEW_NODE = 103; + /** + * 创建并初始化活动 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_list); initResources(); - - /** - * Insert an introduction when user firstly use this application - */ setAppInfoFromRawRes(); } + /** + * 处理从其他活动返回的结果 + */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == RESULT_OK - && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { mNotesListAdapter.changeCursor(null); } else { super.onActivityResult(requestCode, resultCode, data); } } + /** + * 从资源文件中读取并设置应用介绍信息 + */ 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); - char [] buf = new char[1024]; + char[] buf = new char[1024]; int len = 0; while ((len = br.read(buf)) > 0) { sb.append(buf, 0, len); @@ -180,19 +106,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt e.printStackTrace(); return; } finally { - if(in != null) { + if (in != null) { try { in.close(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } } } - WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, - AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, - ResourceParser.RED); + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.RED); note.setWorkingText(sb.toString()); if (note.saveNote()) { sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); @@ -203,19 +126,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 在活动启动时开始异步查询笔记列表 + */ @Override protected void onStart() { super.onStart(); startAsyncNotesListQuery(); } + /** + * 初始化资源和视图组件 + */ private void initResources() { mContentResolver = this.getContentResolver(); mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); mCurrentFolderId = Notes.ID_ROOT_FOLDER; mNotesListView = (ListView) findViewById(R.id.notes_list); - mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), - null, false); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); mNotesListView.setOnItemClickListener(new OnListItemClickListener()); mNotesListView.setOnItemLongClickListener(this); mNotesListAdapter = new NotesListAdapter(this); @@ -231,724 +159,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mModeCallBack = new ModeCallback(); } - private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { - private DropdownMenu mDropDownMenu; - private ActionMode mActionMode; - private MenuItem mMoveMenu; - - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - getMenuInflater().inflate(R.menu.note_list_options, menu); - menu.findItem(R.id.delete).setOnMenuItemClickListener(this); - mMoveMenu = menu.findItem(R.id.move); - if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER - || DataUtils.getUserFolderCount(mContentResolver) == 0) { - mMoveMenu.setVisible(false); - } else { - mMoveMenu.setVisible(true); - mMoveMenu.setOnMenuItemClickListener(this); - } - mActionMode = mode; - mNotesListAdapter.setChoiceMode(true); - mNotesListView.setLongClickable(false); - mAddNewNote.setVisibility(View.GONE); - - View customView = LayoutInflater.from(NotesListActivity.this).inflate( - R.layout.note_list_dropdown_menu, null); - mode.setCustomView(customView); - mDropDownMenu = new DropdownMenu(NotesListActivity.this, - (Button) customView.findViewById(R.id.selection_menu), - R.menu.note_list_dropdown); - mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ - public boolean onMenuItemClick(MenuItem item) { - mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); - updateMenu(); - return true; - } - - }); - return true; - } - - private void updateMenu() { - int selectedCount = mNotesListAdapter.getSelectedCount(); - // Update dropdown menu - String format = getResources().getString(R.string.menu_select_title, selectedCount); - mDropDownMenu.setTitle(format); - MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); - if (item != null) { - if (mNotesListAdapter.isAllSelected()) { - item.setChecked(true); - item.setTitle(R.string.menu_deselect_all); - } else { - item.setChecked(false); - item.setTitle(R.string.menu_select_all); - } - } - } - - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - // TODO Auto-generated method stub - return false; - } - - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - // TODO Auto-generated method stub - return false; - } - - public void onDestroyActionMode(ActionMode mode) { - mNotesListAdapter.setChoiceMode(false); - mNotesListView.setLongClickable(true); - mAddNewNote.setVisibility(View.VISIBLE); - } - - public void finishActionMode() { - mActionMode.finish(); - } - - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { - mNotesListAdapter.setCheckedItem(position, checked); - updateMenu(); - } - - public boolean onMenuItemClick(MenuItem item) { - if (mNotesListAdapter.getSelectedCount() == 0) { - Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), - Toast.LENGTH_SHORT).show(); - return true; - } - - switch (item.getItemId()) { - case R.id.delete: - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setMessage(getString(R.string.alert_message_delete_notes, - mNotesListAdapter.getSelectedCount())); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - batchDelete(); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - break; - case R.id.move: - startQueryDestinationFolders(); - break; - default: - return false; - } - return true; - } - } - - private class NewNoteOnTouchListener implements OnTouchListener { - - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: { - Display display = getWindowManager().getDefaultDisplay(); - int screenHeight = display.getHeight(); - int newNoteViewHeight = mAddNewNote.getHeight(); - int start = screenHeight - newNoteViewHeight; - 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()); - if (view != null && view.getBottom() > start - && (view.getTop() < (start + 94))) { - mOriginY = (int) event.getY(); - mDispatchY = eventY; - event.setLocation(event.getX(), mDispatchY); - mDispatch = true; - return mNotesListView.dispatchTouchEvent(event); - } - } - break; - } - case MotionEvent.ACTION_MOVE: { - if (mDispatch) { - mDispatchY += (int) event.getY() - mOriginY; - event.setLocation(event.getX(), mDispatchY); - return mNotesListView.dispatchTouchEvent(event); - } - break; - } - default: { - if (mDispatch) { - event.setLocation(event.getX(), mDispatchY); - mDispatch = false; - return mNotesListView.dispatchTouchEvent(event); - } - break; - } - } - return false; - } - - }; - - 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"); - } - - private final class BackgroundQueryHandler extends AsyncQueryHandler { - public BackgroundQueryHandler(ContentResolver contentResolver) { - super(contentResolver); - } - - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - switch (token) { - case FOLDER_NOTE_LIST_QUERY_TOKEN: - mNotesListAdapter.changeCursor(cursor); - break; - case FOLDER_LIST_QUERY_TOKEN: - if (cursor != null && cursor.getCount() > 0) { - showFolderListMenu(cursor); - } else { - Log.e(TAG, "Query folder failed"); - } - break; - default: - return; - } - } - } - - private void showFolderListMenu(Cursor cursor) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(R.string.menu_title_select_folder); - final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); - builder.setAdapter(adapter, new DialogInterface.OnClickListener() { - - public void onClick(DialogInterface dialog, int which) { - DataUtils.batchMoveToFolder(mContentResolver, - mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); - Toast.makeText( - NotesListActivity.this, - getString(R.string.format_move_notes_to_folder, - mNotesListAdapter.getSelectedCount(), - adapter.getFolderName(NotesListActivity.this, which)), - Toast.LENGTH_SHORT).show(); - mModeCallBack.finishActionMode(); - } - }); - builder.show(); - } - - private void createNewNote() { - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_INSERT_OR_EDIT); - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); - this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); - } - - private void batchDelete() { - 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"); - } - } - return widgets; - } - - @Override - protected void onPostExecute(HashSet widgets) { - if (widgets != null) { - for (AppWidgetAttribute widget : widgets) { - if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID - && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { - updateWidget(widget.widgetId, widget.widgetType); - } - } - } - mModeCallBack.finishActionMode(); - } - }.execute(); - } - - private void deleteFolder(long folderId) { - if (folderId == Notes.ID_ROOT_FOLDER) { - Log.e(TAG, "Wrong folder id, should not happen " + folderId); - return; - } - - HashSet ids = new HashSet(); - ids.add(folderId); - HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, - folderId); - if (!isSyncMode()) { - // if not synced, delete folder directly - DataUtils.batchDeleteNotes(mContentResolver, ids); - } else { - // in sync mode, we'll move the deleted folder into the trash folder - DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); - } - if (widgets != null) { - for (AppWidgetAttribute widget : widgets) { - if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID - && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { - updateWidget(widget.widgetId, widget.widgetType); - } - } - } - } - - private void openNode(NoteItemData data) { - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, data.getId()); - this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); - } - - private void openFolder(NoteItemData data) { - mCurrentFolderId = data.getId(); - startAsyncNotesListQuery(); - if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mState = ListEditState.CALL_RECORD_FOLDER; - mAddNewNote.setVisibility(View.GONE); - } else { - mState = ListEditState.SUB_FOLDER; - } - if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mTitleBar.setText(R.string.call_record_folder_name); - } else { - mTitleBar.setText(data.getSnippet()); - } - mTitleBar.setVisibility(View.VISIBLE); - } - - public void onClick(View v) { - switch (v.getId()) { - case R.id.btn_new_note: - createNewNote(); - break; - default: - break; - } - } - - private void showSoftInput() { - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) { - inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); - } - } - - private void hideSoftInput(View view) { - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - - private void showCreateOrModifyFolderDialog(final boolean create) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); - final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); - showSoftInput(); - if (!create) { - if (mFocusNoteDataItem != null) { - etName.setText(mFocusNoteDataItem.getSnippet()); - builder.setTitle(getString(R.string.menu_folder_change_name)); - } else { - Log.e(TAG, "The long click data item is null"); - return; - } - } else { - etName.setText(""); - builder.setTitle(this.getString(R.string.menu_create_folder)); - } - - builder.setPositiveButton(android.R.string.ok, null); - builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - hideSoftInput(etName); - } - }); - - final Dialog dialog = builder.setView(view).show(); - final Button positive = (Button)dialog.findViewById(android.R.id.button1); - positive.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - hideSoftInput(etName); - String name = etName.getText().toString(); - if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { - Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), - Toast.LENGTH_LONG).show(); - etName.setSelection(0, etName.length()); - return; - } - if (!create) { - if (!TextUtils.isEmpty(name)) { - ContentValues values = new ContentValues(); - values.put(NoteColumns.SNIPPET, name); - values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); - values.put(NoteColumns.LOCAL_MODIFIED, 1); - mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID - + "=?", new String[] { - String.valueOf(mFocusNoteDataItem.getId()) - }); - } - } else if (!TextUtils.isEmpty(name)) { - ContentValues values = new ContentValues(); - values.put(NoteColumns.SNIPPET, name); - values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); - mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); - } - dialog.dismiss(); - } - }); - - if (TextUtils.isEmpty(etName.getText())) { - positive.setEnabled(false); - } - /** - * When the name edit text is null, disable the positive button - */ - etName.addTextChangedListener(new TextWatcher() { - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // TODO Auto-generated method stub - - } - - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (TextUtils.isEmpty(etName.getText())) { - positive.setEnabled(false); - } else { - positive.setEnabled(true); - } - } - - public void afterTextChanged(Editable s) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void onBackPressed() { - switch (mState) { - case SUB_FOLDER: - mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mState = ListEditState.NOTE_LIST; - startAsyncNotesListQuery(); - mTitleBar.setVisibility(View.GONE); - break; - case CALL_RECORD_FOLDER: - mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mState = ListEditState.NOTE_LIST; - mAddNewNote.setVisibility(View.VISIBLE); - mTitleBar.setVisibility(View.GONE); - startAsyncNotesListQuery(); - break; - case NOTE_LIST: - super.onBackPressed(); - break; - default: - break; - } - } - - private void updateWidget(int appWidgetId, int appWidgetType) { - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - if (appWidgetType == Notes.TYPE_WIDGET_2X) { - intent.setClass(this, NoteWidgetProvider_2x.class); - } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { - intent.setClass(this, NoteWidgetProvider_4x.class); - } else { - Log.e(TAG, "Unspported widget type"); - return; - } - - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - appWidgetId - }); - - sendBroadcast(intent); - setResult(RESULT_OK, intent); - } - - private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - if (mFocusNoteDataItem != null) { - menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); - menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); - menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); - menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); - } - } - }; - - @Override - public void onContextMenuClosed(Menu menu) { - if (mNotesListView != null) { - mNotesListView.setOnCreateContextMenuListener(null); - } - super.onContextMenuClosed(menu); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (mFocusNoteDataItem == null) { - Log.e(TAG, "The long click data item is null"); - return false; - } - switch (item.getItemId()) { - case MENU_FOLDER_VIEW: - openFolder(mFocusNoteDataItem); - break; - case MENU_FOLDER_DELETE: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setMessage(getString(R.string.alert_message_delete_folder)); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - deleteFolder(mFocusNoteDataItem.getId()); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - break; - case MENU_FOLDER_CHANGE_NAME: - showCreateOrModifyFolderDialog(false); - break; - default: - break; - } - - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); - if (mState == ListEditState.NOTE_LIST) { - getMenuInflater().inflate(R.menu.note_list, menu); - // set sync or sync_cancel - menu.findItem(R.id.menu_sync).setTitle( - GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); - } else if (mState == ListEditState.SUB_FOLDER) { - getMenuInflater().inflate(R.menu.sub_folder, menu); - } else if (mState == ListEditState.CALL_RECORD_FOLDER) { - getMenuInflater().inflate(R.menu.call_record_folder, menu); - } else { - Log.e(TAG, "Wrong state:" + mState); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_new_folder: { - showCreateOrModifyFolderDialog(true); - break; - } - case R.id.menu_export_text: { - exportNoteToText(); - break; - } - case R.id.menu_sync: { - if (isSyncMode()) { - if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { - GTaskSyncService.startSync(this); - } else { - GTaskSyncService.cancelSync(this); - } - } else { - startPreferenceActivity(); - } - break; - } - case R.id.menu_setting: { - startPreferenceActivity(); - break; - } - case R.id.menu_new_note: { - createNewNote(); - break; - } - case R.id.menu_search: - onSearchRequested(); - break; - default: - break; - } - return true; - } - - @Override - public boolean onSearchRequested() { - startSearch(null, false, null /* appData */, false); - return true; - } - - private void exportNoteToText() { - final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); - new AsyncTask() { + // 省略其他方法... - @Override - protected Integer doInBackground(Void... unused) { - return backup.exportToText(); - } - - @Override - protected void onPostExecute(Integer result) { - if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.failed_sdcard_export)); - builder.setMessage(NotesListActivity.this - .getString(R.string.error_sdcard_unmounted)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } else if (result == BackupUtils.STATE_SUCCESS) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.success_sdcard_export)); - builder.setMessage(NotesListActivity.this.getString( - R.string.format_exported_file_location, backup - .getExportedTextFileName(), backup.getExportedTextFileDir())); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.failed_sdcard_export)); - builder.setMessage(NotesListActivity.this - .getString(R.string.error_sdcard_export)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } - } - - }.execute(); - } - - private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; - } - - private void startPreferenceActivity() { - Activity from = getParent() != null ? getParent() : this; - Intent intent = new Intent(from, NotesPreferenceActivity.class); - from.startActivityIfNeeded(intent, -1); - } - - private class OnListItemClickListener implements OnItemClickListener { - - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (view instanceof NotesListItem) { - NoteItemData item = ((NotesListItem) view).getItemData(); - if (mNotesListAdapter.isInChoiceMode()) { - if (item.getType() == Notes.TYPE_NOTE) { - position = position - mNotesListView.getHeaderViewsCount(); - mModeCallBack.onItemCheckedStateChanged(null, position, id, - !mNotesListAdapter.isSelectedItem(position)); - } - return; - } - - switch (mState) { - case NOTE_LIST: - if (item.getType() == Notes.TYPE_FOLDER - || item.getType() == Notes.TYPE_SYSTEM) { - openFolder(item); - } else if (item.getType() == Notes.TYPE_NOTE) { - openNode(item); - } else { - Log.e(TAG, "Wrong note type in NOTE_LIST"); - } - break; - case SUB_FOLDER: - case CALL_RECORD_FOLDER: - if (item.getType() == Notes.TYPE_NOTE) { - openNode(item); - } else { - Log.e(TAG, "Wrong note type in SUB_FOLDER"); - } - break; - default: - break; - } - } - } - - } - - private void startQueryDestinationFolders() { - String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; - selection = (mState == ListEditState.NOTE_LIST) ? selection: - "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; - - mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, - null, - Notes.CONTENT_NOTE_URI, - FoldersListAdapter.PROJECTION, - selection, - new String[] { - String.valueOf(Notes.TYPE_FOLDER), - String.valueOf(Notes.ID_TRASH_FOLER), - String.valueOf(mCurrentFolderId) - }, - NoteColumns.MODIFIED_DATE + " DESC"); - } - - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - if (view instanceof NotesListItem) { - mFocusNoteDataItem = ((NotesListItem) view).getItemData(); - if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { - if (mNotesListView.startActionMode(mModeCallBack) != null) { - mModeCallBack.onItemCheckedStateChanged(null, position, id, true); - mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - } else { - Log.e(TAG, "startActionMode fails"); - } - } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { - mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); - } - } - return false; - } } diff --git a/src/net/micode/notes/ui/NotesListAdapter.java b/src/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..73dccc4 100644 --- a/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/net/micode/notes/ui/NotesListAdapter.java @@ -37,6 +37,270 @@ public class NotesListAdapter extends CursorAdapter { private HashMap mSelectedIndex; private int mNotesCount; private boolean mChoiceMode; +/* + * 版权所有 (c) 2010-2011, MiCode开源社区 (www.micode.net) + * + * 根据Apache License 2.0授权许可("许可证"); + * 除非符合许可证规定,否则不得使用此文件。 + * 您可以从以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,否则根据许可证分发的软件按“原样”分发, + * 不提供任何明示或暗示的担保或条件。请参见许可证以了解具体的权限和限制。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * NotesListAdapter 类是用于管理笔记列表视图的适配器,负责将数据从数据库绑定到 ListView 中。 + */ +public class NotesListAdapter extends CursorAdapter { + + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; // 记录选中的项 + private int mNotesCount; // 笔记总数 + private boolean mChoiceMode; // 是否处于多选模式 + + /** + * AppWidgetAttribute 类用于存储小部件属性。 + */ + public static class AppWidgetAttribute { + public int widgetId; // 小部件ID + public int widgetType; // 小部件类型 + } + + /** + * 构造函数,初始化适配器。 + * + * @param context 上下文 + */ + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); + mContext = context; + mNotesCount = 0; + } + + /** + * 创建新的视图项。 + * + * @param context 上下文 + * @param cursor 游标 + * @param parent 父视图 + * @return 新创建的视图 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + /** + * 绑定数据到视图。 + * + * @param view 视图 + * @param context 上下文 + * @param cursor 游标 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + /** + * 设置指定位置的项是否被选中。 + * + * @param position 位置 + * @param checked 是否选中 + */ + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + /** + * 检查是否处于多选模式。 + * + * @return 是否处于多选模式 + */ + public boolean isInChoiceMode() { + return mChoiceMode; + } + + /** + * 设置多选模式。 + * + * @param mode 是否启用多选模式 + */ + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + /** + * 全选或取消全选所有笔记项。 + * + * @param checked 是否选中 + */ + 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); + } + } + } + } + + /** + * 获取所有选中的笔记项ID集合。 + * + * @return 选中的笔记项ID集合 + */ + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "错误的项ID,不应该发生"); + } else { + itemSet.add(id); + } + } + } + return itemSet; + } + + /** + * 获取所有选中的笔记项对应的小部件属性集合。 + * + * @return 小部件属性集合 + */ + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c != null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * 不要在这里关闭游标,只有适配器可以关闭它 + */ + } else { + Log.e(TAG, "无效的游标"); + return null; + } + } + } + return itemSet; + } + + /** + * 获取选中项的数量。 + * + * @return 选中项的数量 + */ + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + /** + * 检查是否所有项都被选中。 + * + * @return 是否所有项都被选中 + */ + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); + } + + /** + * 检查指定位置的项是否被选中。 + * + * @param position 位置 + * @return 是否被选中 + */ + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + /** + * 内容变化时调用。 + */ + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + /** + * 更换游标时调用。 + * + * @param cursor 新游标 + */ + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + /** + * 计算笔记总数。 + */ + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c != null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } else { + Log.e(TAG, "无效的游标"); + return; + } + } + } +} public static class AppWidgetAttribute { public int widgetId; diff --git a/src/net/micode/notes/ui/NotesListItem.java b/src/net/micode/notes/ui/NotesListItem.java index 1221e80..1e15eb0 100644 --- a/src/net/micode/notes/ui/NotesListItem.java +++ b/src/net/micode/notes/ui/NotesListItem.java @@ -1,17 +1,14 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011, MiCode开源社区 (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 + * 根据Apache License 2.0授权许可("许可证"); + * 除非符合许可证规定,否则不得使用此文件。 + * 您可以从以下网址获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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,93 +26,122 @@ import net.micode.notes.data.Notes; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; - +/** + * NotesListItem 类用于表示笔记列表中的每一项视图,负责将数据绑定到视图元素上。 + */ public class NotesListItem extends LinearLayout { - private ImageView mAlert; - private TextView mTitle; - private TextView mTime; - private TextView mCallName; - private NoteItemData mItemData; - private CheckBox mCheckBox; + private ImageView mAlert; // 警告图标 + private TextView mTitle; // 标题文本 + private TextView mTime; // 时间文本 + private TextView mCallName; // 通话记录名称 + private NoteItemData mItemData; // 当前项的数据 + private CheckBox mCheckBox; // 多选框 + + /** + * 构造函数,初始化视图组件。 + * + * @param context 上下文 + */ public NotesListItem(Context context) { super(context); - inflate(context, R.layout.note_item, this); - mAlert = (ImageView) findViewById(R.id.iv_alert_icon); - mTitle = (TextView) findViewById(R.id.tv_title); - mTime = (TextView) findViewById(R.id.tv_time); - mCallName = (TextView) findViewById(R.id.tv_name); - mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + inflate(context, R.layout.note_item, this); // 加载布局文件 + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); // 初始化警告图标 + mTitle = (TextView) findViewById(R.id.tv_title); // 初始化标题文本 + mTime = (TextView) findViewById(R.id.tv_time); // 初始化时间文本 + mCallName = (TextView) findViewById(R.id.tv_name); // 初始化通话记录名称 + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); // 初始化多选框 } + /** + * 绑定数据到视图。 + * + * @param context 上下文 + * @param data 数据对象 + * @param choiceMode 是否处于多选模式 + * @param checked 是否选中 + */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { if (choiceMode && data.getType() == Notes.TYPE_NOTE) { - mCheckBox.setVisibility(View.VISIBLE); - mCheckBox.setChecked(checked); + mCheckBox.setVisibility(View.VISIBLE); // 显示多选框 + mCheckBox.setChecked(checked); // 设置多选框状态 } else { - mCheckBox.setVisibility(View.GONE); + mCheckBox.setVisibility(View.GONE); // 隐藏多选框 } - mItemData = data; + mItemData = data; // 设置当前项的数据 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.GONE); - mAlert.setVisibility(View.VISIBLE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mCallName.setVisibility(View.GONE); // 隐藏通话记录名称 + mAlert.setVisibility(View.VISIBLE); // 显示警告图标 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题样式 mTitle.setText(context.getString(R.string.call_record_folder_name) - + context.getString(R.string.format_folder_files_count, data.getNotesCount())); - mAlert.setImageResource(R.drawable.call_record); + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); // 设置标题文本 + mAlert.setImageResource(R.drawable.call_record); // 设置警告图标资源 } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.VISIBLE); - mCallName.setText(data.getCallName()); - mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mCallName.setVisibility(View.VISIBLE); // 显示通话记录名称 + mCallName.setText(data.getCallName()); // 设置通话记录名称文本 + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); // 设置标题样式 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置标题文本 if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); - mAlert.setVisibility(View.VISIBLE); + mAlert.setImageResource(R.drawable.clock); // 设置警告图标资源 + mAlert.setVisibility(View.VISIBLE); // 显示警告图标 } else { - mAlert.setVisibility(View.GONE); + mAlert.setVisibility(View.GONE); // 隐藏警告图标 } } else { - mCallName.setVisibility(View.GONE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + 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())); - mAlert.setVisibility(View.GONE); + data.getNotesCount())); // 设置标题文本 + mAlert.setVisibility(View.GONE); // 隐藏警告图标 } else { - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置标题文本 if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); - mAlert.setVisibility(View.VISIBLE); + mAlert.setImageResource(R.drawable.clock); // 设置警告图标资源 + mAlert.setVisibility(View.VISIBLE); // 显示警告图标 } else { - mAlert.setVisibility(View.GONE); + mAlert.setVisibility(View.GONE); // 隐藏警告图标 } } } - mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); - setBackground(data); + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); // 设置时间文本 + + setBackground(data); // 设置背景 } + /** + * 设置视图的背景。 + * + * @param data 数据对象 + */ private void setBackground(NoteItemData data) { - int id = data.getBgColorId(); + int id = data.getBgColorId(); // 获取背景颜色ID + if (data.getType() == Notes.TYPE_NOTE) { if (data.isSingle() || data.isOneFollowingFolder()) { - setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); // 设置单个笔记背景 } else if (data.isLast()) { - setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); // 设置最后一个笔记背景 } else if (data.isFirst() || data.isMultiFollowingFolder()) { - setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); // 设置第一个笔记背景 } else { - setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); // 设置普通笔记背景 } } else { - setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); // 设置文件夹背景 } } + /** + * 获取当前项的数据。 + * + * @return 当前项的数据对象 + */ public NoteItemData getItemData() { return mItemData; } diff --git a/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..8a2ddef 100644 --- a/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -1,17 +1,12 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 版权所有 (c) 2010-2011, MiCode开源社区 (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 + * 本文件根据Apache License 2.0授权许可发布。您可以在以下网址获取许可证副本: * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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,35 +42,51 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; - +/** + * NotesPreferenceActivity 类用于管理笔记应用的设置界面,包括同步账户选择、同步按钮操作等。 + */ 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 boolean mHasAddedAccount; + /** + * 创建设置界面时调用的方法,初始化界面元素并注册广播接收器。 + */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - /* using the app icon for navigation */ + // 使用应用图标进行导航 getActionBar().setDisplayHomeAsUpEnabled(true); + // 添加偏好设置资源 addPreferencesFromResource(R.xml.preferences); mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); mReceiver = new GTaskReceiver(); @@ -88,12 +99,14 @@ public class NotesPreferenceActivity extends PreferenceActivity { getListView().addHeaderView(header, null, true); } + /** + * 恢复设置界面时调用的方法,检查是否有新账户并刷新UI。 + */ @Override 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) { @@ -116,6 +129,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { refreshUI(); } + /** + * 销毁设置界面时调用的方法,注销广播接收器。 + */ @Override protected void onDestroy() { if (mReceiver != null) { @@ -124,6 +140,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { super.onDestroy(); } + /** + * 加载账户偏好设置,显示账户选择选项。 + */ private void loadAccountPreference() { mAccountCategory.removeAll(); @@ -135,11 +154,10 @@ public class NotesPreferenceActivity extends PreferenceActivity { 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 { @@ -154,11 +172,14 @@ 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 +197,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); @@ -193,11 +214,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 刷新UI,重新加载账户偏好设置和同步按钮。 + */ private void refreshUI() { loadAccountPreference(); loadSyncButton(); } + /** + * 显示选择账户的对话框。 + */ private void showSelectAccountAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -254,6 +281,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { }); } + /** + * 显示确认更改账户的对话框。 + */ private void showChangeAccountConfirmAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -283,11 +313,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { 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 settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -299,10 +335,10 @@ public class NotesPreferenceActivity extends PreferenceActivity { } editor.commit(); - // clean up last sync time + // 清理上次同步时间 setLastSyncTime(this, 0); - // clean up local gtask related info + // 清理本地与GTask相关的数据 new Thread(new Runnable() { public void run() { ContentValues values = new ContentValues(); @@ -318,6 +354,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 移除同步账户,并清理相关数据。 + */ private void removeSyncAccount() { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); @@ -329,7 +368,7 @@ public class NotesPreferenceActivity extends PreferenceActivity { } editor.commit(); - // clean up local gtask related info + // 清理本地与GTask相关的数据 new Thread(new Runnable() { public void run() { ContentValues values = new ContentValues(); @@ -340,12 +379,18 @@ public class NotesPreferenceActivity extends PreferenceActivity { }).start(); } + /** + * 获取当前设置的同步账户名称。 + */ public static String getSyncAccountName(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); } + /** + * 设置上次同步时间。 + */ public static void setLastSyncTime(Context context, long time) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -354,12 +399,18 @@ public class NotesPreferenceActivity extends PreferenceActivity { editor.commit(); } + /** + * 获取上次同步时间。 + */ public static long getLastSyncTime(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); } + /** + * 广播接收器类,用于处理同步服务的广播消息。 + */ private class GTaskReceiver extends BroadcastReceiver { @Override @@ -374,6 +425,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 处理菜单项选择事件。 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: