From 56bdf6ce081205cfac770e1c9e7f24839fc5ef89 Mon Sep 17 00:00:00 2001 From: zpz1349878361 <1349878361@qq.com> Date: Tue, 31 Dec 2024 22:18:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E5=B0=8F=E7=BB=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/Contact.java | 80 + src/data/Notes.java | 273 ++++ src/data/NotesDatabaseHelper.java | 381 +++++ src/data/NotesProvider.java | 358 +++++ src/gtask/data/MetaData.java | 125 ++ src/gtask/data/Node.java | 123 ++ src/gtask/data/SqlData.java | 222 +++ src/gtask/data/SqlNote.java | 513 +++++++ src/gtask/data/Task.java | 415 ++++++ src/gtask/data/TaskList.java | 463 ++++++ .../exception/ActionFailureException.java | 53 + .../exception/NetworkFailureException.java | 47 + src/gtask/remote/GTaskASyncTask.java | 152 ++ src/gtask/remote/GTaskClient.java | 1234 ++++++++++++++++ src/gtask/remote/GTaskManager.java | 948 ++++++++++++ src/gtask/remote/GTaskSyncService.java | 169 +++ src/model/Note.java | 254 ++++ src/model/WorkingNote.java | 289 ++++ src/tool/BackupUtils.java | 279 ++++ src/tool/DataUtils.java | 406 +++++ src/tool/GTaskStringUtils.java | 64 + src/tool/ResourceParser.java | 212 +++ src/ui/AlarmAlertActivity.java | 196 +++ src/ui/AlarmInitReceiver.java | 71 + src/ui/AlarmReceiver.java | 30 + src/ui/DateTimePicker.java | 583 ++++++++ src/ui/DateTimePickerDialog.java | 121 ++ src/ui/DropdownMenu.java | 73 + src/ui/FoldersListAdapter.java | 110 ++ src/ui/NoteEditActivity.java | 1141 ++++++++++++++ src/ui/NoteEditText.java | 267 ++++ src/ui/NoteItemData.java | 257 ++++ src/ui/NoteWidgetProvider.java | 169 +++ src/ui/Notes.java | 273 ++++ src/ui/NotesDatabaseHelper.java | 381 +++++ src/ui/NotesListActivity.java | 1305 +++++++++++++++++ src/ui/NotesListAdapter.java | 269 ++++ src/ui/NotesListItem.java | 157 ++ src/ui/NotesPreferenceActivity.java | 481 ++++++ src/widget/NoteWidgetProvider.java | 159 ++ src/widget/NoteWidgetProvider_2x.java | 76 + src/widget/NoteWidgetProvider_4x.java | 74 + 42 files changed, 13253 insertions(+) create mode 100644 src/data/Contact.java create mode 100644 src/data/Notes.java create mode 100644 src/data/NotesDatabaseHelper.java create mode 100644 src/data/NotesProvider.java create mode 100644 src/gtask/data/MetaData.java create mode 100644 src/gtask/data/Node.java create mode 100644 src/gtask/data/SqlData.java create mode 100644 src/gtask/data/SqlNote.java create mode 100644 src/gtask/data/Task.java create mode 100644 src/gtask/data/TaskList.java create mode 100644 src/gtask/exception/ActionFailureException.java create mode 100644 src/gtask/exception/NetworkFailureException.java create mode 100644 src/gtask/remote/GTaskASyncTask.java create mode 100644 src/gtask/remote/GTaskClient.java create mode 100644 src/gtask/remote/GTaskManager.java create mode 100644 src/gtask/remote/GTaskSyncService.java create mode 100644 src/model/Note.java create mode 100644 src/model/WorkingNote.java create mode 100644 src/tool/BackupUtils.java create mode 100644 src/tool/DataUtils.java create mode 100644 src/tool/GTaskStringUtils.java create mode 100644 src/tool/ResourceParser.java create mode 100644 src/ui/AlarmAlertActivity.java create mode 100644 src/ui/AlarmInitReceiver.java create mode 100644 src/ui/AlarmReceiver.java create mode 100644 src/ui/DateTimePicker.java create mode 100644 src/ui/DateTimePickerDialog.java create mode 100644 src/ui/DropdownMenu.java create mode 100644 src/ui/FoldersListAdapter.java create mode 100644 src/ui/NoteEditActivity.java create mode 100644 src/ui/NoteEditText.java create mode 100644 src/ui/NoteItemData.java create mode 100644 src/ui/NoteWidgetProvider.java create mode 100644 src/ui/Notes.java create mode 100644 src/ui/NotesDatabaseHelper.java create mode 100644 src/ui/NotesListActivity.java create mode 100644 src/ui/NotesListAdapter.java create mode 100644 src/ui/NotesListItem.java create mode 100644 src/ui/NotesPreferenceActivity.java create mode 100644 src/widget/NoteWidgetProvider.java create mode 100644 src/widget/NoteWidgetProvider_2x.java create mode 100644 src/widget/NoteWidgetProvider_4x.java diff --git a/src/data/Contact.java b/src/data/Contact.java new file mode 100644 index 0000000..45ad20d --- /dev/null +++ b/src/data/Contact.java @@ -0,0 +1,80 @@ +/* + * Contact 类用于通过电话号码查询联系人信息。 + * 该类实现了从联系人数据库中获取与特定电话号码相关联的显示名称。 + */ + +package net.micode.notes.data; + +import android.content.Context; +import android.database.Cursor; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Data; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import java.util.HashMap; + +public class Contact { + // 缓存已查询过的电话号码和对应的联系人名称,以减少数据库查询次数。 + private static HashMap sContactCache; + private static final String TAG = "Contact"; // 日志标签 + + // 用于查询具有完整国际号码格式的电话号码的selection字符串。 + private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" + + " AND " + Data.RAW_CONTACT_ID + " IN " + + "(SELECT raw_contact_id " + + " FROM phone_lookup" + + " WHERE min_match = '+')"; + + /** + * 根据电话号码获取联系人名称。 + * + * @param context 上下文对象,用于访问内容解析器。 + * @param phoneNumber 需要查询的电话号码。 + * @return 与电话号码相关联的联系人名称,如果找不到则返回null。 + */ + public static String getContact(Context context, String phoneNumber) { + // 初始化或获取联系人缓存 + if (sContactCache == null) { + sContactCache = new HashMap(); + } + + // 从缓存中直接获取联系人名称,如果存在。 + if (sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + + // 使用PhoneNumberUtils将电话号码格式化为适合查询的形式 + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + + // 执行查询以获取与电话号码相关联的联系人名称 + Cursor cursor = context.getContentResolver().query( + Data.CONTENT_URI, + new String[]{Phone.DISPLAY_NAME}, + selection, + new String[]{phoneNumber}, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + // 从查询结果中获取联系人名称并加入缓存 + String name = cursor.getString(0); + sContactCache.put(phoneNumber, name); + return name; + } catch (IndexOutOfBoundsException e) { + // 处理查询结果异常 + Log.e(TAG, " Cursor get string error " + e.toString()); + return null; + } finally { + // 关闭游标 + cursor.close(); + } + } else { + // 如果查询无结果,记录日志 + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; + } + } +} diff --git a/src/data/Notes.java b/src/data/Notes.java new file mode 100644 index 0000000..226763b --- /dev/null +++ b/src/data/Notes.java @@ -0,0 +1,273 @@ +/* + * 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.data; + +import android.net.Uri; + +// Notes类定义了与笔记和文件夹相关的常量和数据列接口 +public class Notes { + public static final String AUTHORITY = "micode_notes"; // 用于标识内容提供者的授权名称 + public static final String TAG = "Notes"; // 日志标签 + public static final int TYPE_NOTE = 0; // 笔记类型 + public static final int TYPE_FOLDER = 1; // 文件夹类型 + public static final int TYPE_SYSTEM = 2; // 系统类型 + + /** + * 下面的ID是系统文件夹的标识符 + * {@link Notes#ID_ROOT_FOLDER} 是默认文件夹 + * {@link Notes#ID_TEMPARAY_FOLDER} 是属于没有文件夹的笔记 + * {@link Notes#ID_CALL_RECORD_FOLDER} 是用于存储通话记录的 + */ + public static final int ID_ROOT_FOLDER = 0; // 根文件夹ID + public static final int ID_TEMPARAY_FOLDER = -1; // 临时文件夹ID,用于存放不属于任何文件夹的笔记 + public static final int ID_CALL_RECORD_FOLDER = -2; // 通话记录文件夹ID + public static final int ID_TRASH_FOLER = -3; // 垃圾箱文件夹ID + + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; // 用于Intent的提醒日期额外数据 + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; // 笔记背景色ID + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; // 小部件ID + public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; // 小部件类型 + public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; // 文件夹ID + public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; // 通话日期 + + public static final int TYPE_WIDGET_INVALIDE = -1; // 无效的小部件类型 + public static final int TYPE_WIDGET_2X = 0; // 2x小部件类型 + public static final int TYPE_WIDGET_4X = 1; // 4x小部件类型 + + public static class DataConstants { + public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; // 笔记的内容项类型 + public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; // 通话记录的内容项类型 + } + + /** + * 查询所有笔记和文件夹的Uri + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * 查询数据的Uri + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + // 笔记和文件夹的公共列接口 + public interface NoteColumns { + /** + * 行的唯一ID + *

类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 笔记或文件夹的父ID + *

类型: INTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * 创建日期 + *

类型: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 最后修改日期 + *

类型: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 提醒日期 + *

类型: INTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * 笔记或文件夹的摘要信息 + *

类型: TEXT

+ */ + public static final String SNIPPET = "snippet"; + + /** + * 笔记的小部件ID + *

类型: INTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * 笔记的小部件类型 + *

类型: INTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * 笔记的背景色ID + *

类型: INTEGER (long)

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * 笔记是否有附件 + *

类型: INTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * 笔记数量 + *

类型: INTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * 文件夹类型:0-笔记,1-文件夹 + *

类型: INTEGER

+ */ + public static final String TYPE = "type"; + + /** + * 最后同步ID + *

类型: INTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * 标记本地是否已修改 + *

类型: INTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * 移入临时文件夹前的原始父ID + *

类型: INTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * Google任务ID + *

类型: TEXT

+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * 版本号 + *

类型: INTEGER (long)

+ */ + public static final String VERSION = "version"; + } + + // 数据列接口 + public interface DataColumns { + /** + * 行的唯一ID + *

类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 该项的MIME类型。 + *

类型: TEXT

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * 属于的笔记的引用ID + *

类型: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 创建日期 + *

类型: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 最后修改日期 + *

类型: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 数据内容 + *

类型: TEXT

+ */ + public static final String CONTENT = "content"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储整型数据 + *

类型: INTEGER

+ */ + public static final String DATA1 = "data1"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储整型数据 + *

类型: INTEGER

+ */ + public static final String DATA2 = "data2"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储文本数据 + *

类型: TEXT

+ */ + public static final String DATA3 = "data3"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储文本数据 + *

类型: TEXT

+ */ + public static final String DATA4 = "data4"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储文本数据 + *

类型: TEXT

+ */ + public static final String DATA5 = "data5"; + } + + // 文本笔记类,实现了DataColumns接口 + public static final class TextNote implements DataColumns { + /** + * 模式,指示文本是否在检查列表模式中 + *

类型: INTEGER 1:检查列表模式 0: 正常模式

+ */ + public static final String MODE = DATA1; + + public static final int MODE_CHECK_LIST = 1; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; // MIME类型定义 + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; // 单项MIME类型定义 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); // 内容URI定义 + } + + // 通话记录笔记类,实现了DataColumns接口 + public static final class CallNote implements DataColumns { + /** + * 通话日期 + *

类型: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; + + /** + * 电话号码 + *

类型: TEXT

+ */ + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; // MIME类型定义 + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; // 单项MIME类型定义 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); // 内容URI定义 + } +} diff --git a/src/data/NotesDatabaseHelper.java b/src/data/NotesDatabaseHelper.java new file mode 100644 index 0000000..36973ec --- /dev/null +++ b/src/data/NotesDatabaseHelper.java @@ -0,0 +1,381 @@ +/* + * 该类为Notes数据库的辅助类,负责管理数据库的创建和版本管理。 + */ +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + + +public class NotesDatabaseHelper extends SQLiteOpenHelper { + // 数据库名称·· + private static final String DB_NAME = "note.db"; + + // 数据库版本号 + private static final int DB_VERSION = 4; + + // 表接口,定义了数据库中的两个表名 + public interface TABLE { + public static final String NOTE = "note"; + + public static final String DATA = "data"; + } + + // 日志标签 + private static final String TAG = "NotesDatabaseHelper"; + + // 单例模式,确保数据库辅助类的唯一实例 + private static NotesDatabaseHelper mInstance; + + // 创建NOTE表的SQL语句 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + // 创建DATA表的SQL语句 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + // 创建DATA表的NOTE_ID索引的SQL语句 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + // 当更新NOTE表中的PARENT_ID字段时,增加目标文件夹的NOTE_COUNT + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + // 当从文件夹移动NOTE时,减少源文件夹的NOTE_COUNT + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + // 当插入新NOTE时,增加目标文件夹的NOTE_COUNT + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + // 当删除NOTE时,减少文件夹的NOTE_COUNT + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + // 当插入DATA时,如果类型为NOTE,则更新关联NOTE的内容 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + // 当更新DATA时,如果类型为NOTE,则更新关联NOTE的内容 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + // 当删除DATA时,如果类型为NOTE,则更新关联NOTE的内容为空 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + // 当删除NOTE时,删除关联的DATA + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 当删除NOTE时,删除属于该NOTE的子NOTE + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 当NOTE移动到回收站文件夹时,将所有子NOTE也移动到回收站 + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 构造函数,私有化以防止外部实例化 + * context 上下文对象,用于访问应用的资源和其他组件 + */ + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + /** + * 创建NOTE表,并重新创建NOTE表的触发器,然后创建系统文件夹 + *db SQLiteDatabase对象,用于执行SQL创建语句 + */ + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); + reCreateNoteTableTriggers(db); + createSystemFolder(db); + Log.d(TAG, "note table has been created"); + } + + /** + * 重新创建笔记表的触发器 + * db SQLiteDatabase 类型,数据库对象 + */ + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + // 删除旧的触发器 + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + // 创建新的触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + /** + * 创建系统文件夹 + * db SQLiteDatabase 类型,数据库对象 + */ + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + // 创建通话记录文件夹 + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建根文件夹(默认文件夹) + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建临时文件夹,用于移动笔记 + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建回收站文件夹 + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 创建数据表 + * db SQLiteDatabase 类型,数据库对象 + */ + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); + reCreateDataTableTriggers(db); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + Log.d(TAG, "data table has been created"); + } + + /** + * 重新创建数据表的触发器 + *db SQLiteDatabase 类型,数据库对象 + */ + private void reCreateDataTableTriggers(SQLiteDatabase db) { + // 删除旧的触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + // 创建新的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + /** + * 获取 NotesDatabaseHelper 的单例对象 + *context Context 类型,应用上下文 + *NotesDatabaseHelper 类型,单例对象 + */ + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + /** + * 创建数据库表 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + + /** + * 升级数据库 + * + * @param db SQLiteDatabase 类型,数据库对象 + * @param oldVersion int 类型,旧版本号 + * @param newVersion int 类型,新版本号 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + // 根据旧版本号逐步升级 + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // 这次升级包括从 v2 到 v3 的升级 + oldVersion++; + } + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + if (oldVersion == 3) { + upgradeToV4(db); + oldVersion++; + } + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + /** + * 从版本1升级到版本2 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + private void upgradeToV2(SQLiteDatabase db) { + // 删除旧表,创建新表 + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + /** + * 从版本2升级到版本3 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + private void upgradeToV3(SQLiteDatabase db) { + // 删除未使用的触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // 添加一个用于 gtask id 的列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // 添加一个回收站系统文件夹 + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 从版本3升级到版本4 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + private void upgradeToV4(SQLiteDatabase db) { + // 添加版本号列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} \ No newline at end of file diff --git a/src/data/NotesProvider.java b/src/data/NotesProvider.java new file mode 100644 index 0000000..c38d7b3 --- /dev/null +++ b/src/data/NotesProvider.java @@ -0,0 +1,358 @@ +/* + * 该类是Notes应用的内容提供者,负责管理Notes的数据,包括查询、插入、更新和删除操作。 + * 它与数据库交互,将操作转换为对数据库的相应操作。 + */ +package net.micode.notes.data; + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; + + +public class NotesProvider extends ContentProvider { + private static final UriMatcher mMatcher; + + private NotesDatabaseHelper mHelper; + + private static final String TAG = "NotesProvider"; + + private static final int URI_NOTE = 1; + private static final int URI_NOTE_ITEM = 2; + private static final int URI_DATA = 3; + private static final int URI_DATA_ITEM = 4; + + private static final int URI_SEARCH = 5; + private static final int URI_SEARCH_SUGGEST = 6; + + // 初始化UriMatcher,用于匹配不同的URI请求 + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); + } + + /** + * 在搜索结果中,为了显示更多信息,我们会去除标题和内容中的'\n'和空白字符。 + */ + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + + // 用于搜索查询的SQL语句 + private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + /** + * 当ContentProvider被创建时调用,用于初始化数据库帮助类。 + * + * @return 总是返回true。 + */ + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext()); + return true; + } + + /** + * 根据URI查询数据。 + * + * @param uri 要查询的数据的URI。 + * @param projection 要返回的列。 + * @param selection 查询条件。 + * @param selectionArgs 用于查询条件的参数。 + * @param sortOrder 排序顺序。 + * @return 返回匹配的Cursor对象。 + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Cursor c = null; + SQLiteDatabase db = mHelper.getReadableDatabase(); + String id = null; + // 根据URI匹配查询类型 + switch (mMatcher.match(uri)) { + case URI_NOTE: + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + // 处理搜索建议的特殊逻辑 + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); + } + } else { + searchString = uri.getQueryParameter("pattern"); + } + + if (TextUtils.isEmpty(searchString)) { + return null; + } + + try { + searchString = String.format("%%%s%%", searchString); + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, + new String[]{searchString}); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + // 设置通知URI,以便数据改变时可以通知 + if (c != null) { + c.setNotificationUri(getContext().getContentResolver(), uri); + } + return c; + } + + /** + * 在数据库中插入新数据。 + * + * @param uri 插入数据的URI。 + * @param values 要插入的数据。 + * @return 返回插入数据的URI。 + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); + long dataId = 0, noteId = 0, insertedId = 0; + switch (mMatcher.match(uri)) { + case URI_NOTE: + insertedId = noteId = db.insert(TABLE.NOTE, null, values); + break; + case URI_DATA: + if (values.containsKey(DataColumns.NOTE_ID)) { + noteId = values.getAsLong(DataColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + insertedId = dataId = db.insert(TABLE.DATA, null, values); + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + // 通知URI改变 + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + return ContentUris.withAppendedId(uri, insertedId); + } + + /** + * 根据URI删除数据。 + * + * @param uri 要删除数据的URI。 + * @param selection 删除条件。 + * @param selectionArgs 用于删除条件的参数。 + * @return 返回被删除的行数。 + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean deleteData = false; + switch (mMatcher.match(uri)) { + case URI_NOTE: + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + /** + * ID小于等于0的笔记是系统文件夹,不允许删除 + */ + long noteId = Long.valueOf(id); + if (noteId <= 0) { + break; + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.DATA, + DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + deleteData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + // 通知URI改变 + if (count > 0) { + if (deleteData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + /** + * 更新数据库中的数据。 + * + * @param uri 要更新数据的URI。 + * @param values 要更新到的数据。 + * @param selection 更新条件。 + * @param selectionArgs 用于更新条件的参数。 + * @return 返回被更新的行数。 + */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean updateData = false; + switch (mMatcher.match(uri)) { + case URI_NOTE: + increaseNoteVersion(-1, selection, selectionArgs); + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); + count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + updateData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // 通知URI改变 + if (count > 0) { + if (updateData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + + /** + * 解析选择条件,如果存在选择条件,则在条件前后添加" AND (" 和 ')'。 + * + * @param selection 用户提供的选择条件。 + * @return 如果有选择条件,则返回添加了定界符的选择条件字符串;否则返回空字符串。 + */ + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } + + /** + * 增加指定笔记的版本号。 + * + * @param id 笔记的ID,如果为正数,则根据ID更新版本号;如果为0或负数,则根据selection参数更新版本号。 + * @param selection 用于选择需要更新的笔记的条件字符串,可以为空。 + * @param selectionArgs 与selection参数配合使用的参数数组,用于替换selection中的"?"。 + */ + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); + + // 根据ID或选择条件构建SQL的WHERE子句 + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); + } + if (!TextUtils.isEmpty(selection)) { + String selectString = id > 0 ? parseSelection(selection) : selection; + // 替换selection中的"?"为selectionArgs中的对应参数 + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); + } + sql.append(selectString); + } + + mHelper.getWritableDatabase().execSQL(sql.toString()); + } + + /** + * 根据URI获取对应的MIME类型。 + * 本方法是个待实现的方法,当前仅返回null。 + * + * @param uri 请求的URI。 + * @return 返回null,表示该方法尚未实现。 + */ + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/src/gtask/data/MetaData.java b/src/gtask/data/MetaData.java new file mode 100644 index 0000000..1a9f7f6 --- /dev/null +++ b/src/gtask/data/MetaData.java @@ -0,0 +1,125 @@ +/** + * MetaData类,继承自Task类,用于处理与任务相关的元数据。 + * 这个类主要用于存储和管理与任务相关的元信息,包括全局ID和元数据内容处理。 + */ +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); // 日志标签,用于调试和错误管理 + + private String mRelatedGid = null; // 与任务相关的全局ID + + /** + * 设置元数据。 + * + * @param gid 任务的全局ID。此ID用于在系统中唯一标识任务。 + * @param metaInfo 元信息的JSON对象,包含与当前任务相关的其他信息。 + */ + public void setMeta(String gid, JSONObject metaInfo) { + try { + // 将任务的全局ID添加到元信息中 + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + // 记录错误日志,表示在将ID添加到元信息时失败 + Log.e(TAG, "failed to put related gid"); + } + + // 将元信息设置为任务的笔记,以JSON字符串的形式存储在笔记字段中 + setNotes(metaInfo.toString()); + // 设置任务的名称为特定的元数据标志名称 + setName(GTaskStringUtils.META_NOTE_NAME); + } + + /** + * 获取与任务相关的全局ID。 + * + * @return 相关的全局ID字符串。如果没有设置,则返回null。 + */ + public String getRelatedGid() { + return mRelatedGid; + } + + /** + * 判断任务是否值得保存。 + * + * @return 如果任务的笔记字段不为空,则返回true,表示任务值得保存。 + * 否则返回false,表示任务不需要保存。 + */ + @Override + public boolean isWorthSaving() { + // 检查笔记是否为null,以决定是否值得保存 + return getNotes() != null; + } + + /** + * 通过远程JSON对象设置内容。 + * + * @param js JSON对象,包含远程任务的内容。 + * 客户端可通过此方法将任务的内容与远程数据同步。 + */ + @Override + public void setContentByRemoteJSON(JSONObject js) { + // 调用父类的方法设置标准内容 + super.setContentByRemoteJSON(js); + + if (getNotes() != null) { + try { + // 从笔记中提取元信息的JSON对象 + JSONObject metaInfo = new JSONObject(getNotes().trim()); + // 提取相关的全局ID + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + // 记录警告,表示从笔记中无法获取相关ID + Log.w(TAG, "failed to get related gid"); + // 提取失败时,设置相关ID为null + mRelatedGid = null; + } + } + } + + /** + * 通过本地JSON对象设置内容。此方法不应被调用。 + * + * @param js 本地JSON对象,将本地的任务内容赋值给任务属性。 + * @throws IllegalAccessError 本方法不应该被调用,调用时抛出该异常。 + */ + @Override + public void setContentByLocalJSON(JSONObject js) { + // 本方法不应被调用,调用时抛出异常 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + /** + * 从内容生成本地JSON对象。此方法不应被调用。 + * + * @return 生成的JSON对象,此方法生成的JSON对象用以在本地保存或传输。 + * @throws IllegalAccessError 本方法不应该被调用,调用时抛出该异常。 + */ + @Override + public JSONObject getLocalJSONFromContent() { + // 本方法不应被调用,调用时抛出异常 + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + /** + * 获取同步操作类型。此方法不应被调用。 + * + * @param c 数据库游标,指向当前任务,用以获取任务的同步状态。 + * @return 同步操作的类型,例如新增、更新或删除等。 + * @throws IllegalAccessError 本方法不应该被调用,调用时抛出该异常。 + */ + @Override + public int getSyncAction(Cursor c) { + // 本方法不应被调用,调用时抛出异常 + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } +} + diff --git a/src/gtask/data/Node.java b/src/gtask/data/Node.java new file mode 100644 index 0000000..a95fe42 --- /dev/null +++ b/src/gtask/data/Node.java @@ -0,0 +1,123 @@ +/* + * Node类定义了一个节点的基本属性和操作,用于数据同步时的表现和转换。 + * 它是用于表示通用数据节点的抽象类,具体的数据操作和格式转换由其子类实现。 + */ + +package net.micode.notes.gtask.data; + +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; // 初始时GID为空 + mName = ""; // 节点名称初始化为空字符串 + mLastModified = 0; // 最后修改时间初始化为0(未设置) + mDeleted = false; // 节点初始状态为未删除 + } + + /** + * 生成创建节点的JSON动作。 + * + * @param actionId 同步操作的标识符,表示当前的操作类型。 + * @return 包含创建节点信息的JSON对象。 + */ + public abstract JSONObject getCreateAction(int actionId); + + /** + * 生成更新节点的JSON动作。 + * + * @param actionId 同步操作的标识符,表示当前的操作类型。 + * @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(); + + /** + * 根据Cursor获取同步动作。 + * + * @param c 数据库游标,当前指向节点记录。 + * @return 返回对应同步操作的类型。 + */ + 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; + } + + // 获取节点名称 + public String getName() { + return this.mName; + } + + // 获取节点最后修改时间 + public long getLastModified() { + return this.mLastModified; + } + + // 获取节点是否被删除的标志 + public boolean getDeleted() { + return this.mDeleted; + } + +} diff --git a/src/gtask/data/SqlData.java b/src/gtask/data/SqlData.java new file mode 100644 index 0000000..0143954 --- /dev/null +++ b/src/gtask/data/SqlData.java @@ -0,0 +1,222 @@ +/* + * SqlData 类用于操作和管理数据库中的数据项。 + * 提供了从 JSON 对象设置内容,从数据库 Cursor 加载数据,以及提交数据更新到数据库的功能。 + */ + +package net.micode.notes.gtask.data; + +import android.content.ContentResolver; +import android.content.ContentUris; +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.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; + +public class SqlData { + // 日志标签 + private static final String TAG = SqlData.class.getSimpleName(); + + // 无效ID常量 + private static final int INVALID_ID = -99999; + + // 查询时使用的字段投影 + public static final String[] PROJECTION_DATA = new String[]{ + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + + // 字段在Cursor中的索引 + public static final int DATA_ID_COLUMN = 0; + public static final int DATA_MIME_TYPE_COLUMN = 1; + public static final int DATA_CONTENT_COLUMN = 2; + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + + // ContentResolver用于操作内容提供者 + private ContentResolver mContentResolver; + + // 标记当前对象是创建状态还是更新状态 + private boolean mIsCreate; + + // 数据项ID + private long mDataId; + + // 数据项的MIME类型 + private String mDataMimeType; + + // 数据项的内容 + private String mDataContent; + + // 数据项的附加数据1 + private long mDataContentData1; + + // 数据项的附加数据3 + private String mDataContentData3; + + // 存储与数据库不同步的数据变化 + private ContentValues mDiffDataValues; + + /* + * SqlData 构造函数,用于创建新的数据项。 + * @param context 上下文对象,用于获取ContentResolver。 + */ + public SqlData(Context context) { + mContentResolver = context.getContentResolver(); + mIsCreate = true; // 设置为创建状态 + mDataId = INVALID_ID; // 初始化为无效ID + mDataMimeType = DataConstants.NOTE; // 默认MIME类型为笔记 + mDataContent = ""; // 内容初始化为空 + mDataContentData1 = 0; // 附加数据1初始化为0 + mDataContentData3 = ""; // 附加数据3初始化为空 + mDiffDataValues = new ContentValues(); // 初始化差异数据值 + } + + /* + * SqlData 构造函数,用于加载现有数据项。 + * @param context 上下文对象,用于获取ContentResolver。 + * @param c 数据项的Cursor对象,用于加载数据。 + */ + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); + mIsCreate = false; // 设置为更新状态 + loadFromCursor(c); // 从Cursor中加载数据 + mDiffDataValues = new ContentValues(); // 初始化差异数据值 + } + + /* + * 从Cursor中加载数据。 + * @param c 数据项的Cursor对象。 + */ + private void loadFromCursor(Cursor c) { + mDataId = c.getLong(DATA_ID_COLUMN); // 获取数据项ID + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); // 获取MIME类型 + mDataContent = c.getString(DATA_CONTENT_COLUMN); // 获取内容 + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); // 获取附加数据1 + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); // 获取附加数据3 + } + + /* + * 根据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; // 获取ID + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); // 保存ID变化 + } + mDataId = dataId; // 设置ID + + String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) + : DataConstants.NOTE; // 获取MIME类型 + if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { + mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); // 保存MIME类型变化 + } + mDataMimeType = dataMimeType; // 设置MIME类型 + + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; // 获取内容 + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); // 保存内容变化 + } + mDataContent = dataContent; // 设置内容 + + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; // 获取附加数据1 + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); // 保存附加数据1变化 + } + mDataContentData1 = dataContentData1; // 设置附加数据1 + + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; // 获取附加数据3 + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); // 保存附加数据3变化 + } + mDataContentData3 = dataContentData3; // 设置附加数据3 + } + + /* + * 获取数据项的内容,转换为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"); // 输出错误信息 + return null; // 如果未创建则返回null + } + JSONObject js = new JSONObject(); // 创建JSON对象 + js.put(DataColumns.ID, mDataId); // 加入ID + js.put(DataColumns.MIME_TYPE, mDataMimeType); // 加入MIME类型 + js.put(DataColumns.CONTENT, mDataContent); // 加入内容 + js.put(DataColumns.DATA1, mDataContentData1); // 加入附加数据1 + js.put(DataColumns.DATA3, mDataContentData3); // 加入附加数据3 + return js; // 返回JSON对象 + } + + /* + * 将数据项提交到数据库,如果是新数据项则插入,否则更新。 + * @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); // 从差异中移除ID + } + + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); // 设置笔记ID + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); // 插入数据项 + try { + mDataId = Long.valueOf(uri.getPathSegments().get(1)); // 从URI中获取新数据项的ID + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); // 输出错误 + throw new ActionFailureException("create note failed"); // 抛出异常表示创建失败 + } + } else { + // 处理现有数据项的更新 + if (mDiffDataValues.size() > 0) { + int result = 0; + if (!validateVersion) { + // 不验证版本号时直接更新 + result = mContentResolver.update(ContentUris.withAppendedId( + 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 + + " 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"); // 输出警告 + } + } + } + + // 清理并重置状态 + mDiffDataValues.clear(); // 清空差异数据 + mIsCreate = false; // 设置为更新状态 + } + + /* + * 获取数据项的ID。 + * @return 数据项的ID。 + */ + public long getId() { + return mDataId; // 返回数据项ID + } +} diff --git a/src/gtask/data/SqlNote.java b/src/gtask/data/SqlNote.java new file mode 100644 index 0000000..23ce7fb --- /dev/null +++ b/src/gtask/data/SqlNote.java @@ -0,0 +1,513 @@ +/* + * 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.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; + +/** + * SqlNote 类用于管理和操作数据库中的笔记数据。 + * 它提供了创建、加载和更新笔记内容的接口。 + */ +public class SqlNote { + // 日志标签,便于调试和输出日志 + private static final String TAG = SqlNote.class.getSimpleName(); + + // 无效的ID值,用于标识未定义或错误的ID + private static final int INVALID_ID = -99999; + + // 查询笔记时要选择的列 + 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, + NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, + NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, + 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; // 笔记ID + private long mAlertDate; // 提醒日期 + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建日期 + private int mHasAttachment; // 是否有附件标志 + private long mModifiedDate; // 最后修改日期 + private long mParentId; // 父笔记ID + private String mSnippet; // 笔记摘录 + private int mType; // 笔记类型 + private int mWidgetId; // 相关的小部件ID + private int mWidgetType; // 小部件类型 + private long mOriginParent; // 原父笔记ID + private long mVersion; // 笔记版本号 + + // 用于存储两次更新之间差异的数据值 + private ContentValues mDiffNoteValues; + + // 存储与笔记相关数据的列表 + private ArrayList mDataList; + + /** + * 构造函数,初始化一个新的SqlNote实例。 + * + * @param context 上下文,通常是指Activity或Application对象。 + */ + public SqlNote(Context context) { + mContext = context; // 获取上下文 + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = true; // 设为创建状态 + // 初始化笔记属性为默认值 + mId = INVALID_ID; // 初始化ID为无效ID + mAlertDate = 0; // 提醒日期初始化为0 + mBgColorId = ResourceParser.getDefaultBgId(context); // 获取默认背景颜色ID + mCreatedDate = System.currentTimeMillis(); // 设置创建日期为当前时间 + mHasAttachment = 0; // 初始化附件标志为0(无附件) + mModifiedDate = System.currentTimeMillis(); // 同样设置最后修改日期为当前时间 + mParentId = 0; // 父ID初始化为0 + mSnippet = ""; // 笔记摘录初始化为空字符串 + mType = Notes.TYPE_NOTE; // 默认类型设置为普通笔记 + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; // 小部件ID初始化为无效ID + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 小部件类型初始化为无效类型 + mOriginParent = 0; // 原父ID初始化为0 + mVersion = 0; // 版本号初始化为0 + mDiffNoteValues = new ContentValues(); // 初始化差异数据字典 + mDataList = new ArrayList(); // 初始化与笔记相关的数据列表 + } + + /** + * 构造函数,从数据库中加载指定ID的笔记。 + * + * @param context 上下文,通常是指Activity或Application对象。 + * @param c 数据库查询结果的Cursor对象。 + */ + public SqlNote(Context context, Cursor c) { + mContext = context; // 获取上下文 + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = false; // 设为更新状态 + loadFromCursor(c); // 从Cursor中加载笔记数据 + mDataList = new ArrayList(); // 初始化与笔记相关的数据列表 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); // 如果是笔记类型,则加载数据内容 + mDiffNoteValues = new ContentValues(); // 初始化差异数据字典 + } + + /** + * 构造函数,从数据库中加载指定ID的笔记。 + * + * @param context 上下文,通常是指Activity或Application对象。 + * @param id 要加载的笔记的ID。 + */ + public SqlNote(Context context, long id) { + mContext = context; // 获取上下文 + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = false; // 设为更新状态 + loadFromCursor(id); // 根据ID从数据库加载数据 + mDataList = new ArrayList(); // 初始化与笔记相关的数据列表 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); // 如果是笔记类型,则加载数据内容 + mDiffNoteValues = new ContentValues(); // 初始化差异数据字典 + } + + // 从数据库中加载笔记数据 + private void loadFromCursor(long id) { + Cursor c = null; + try { + // 根据ID查询笔记数据 + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", + new String[]{ String.valueOf(id) }, null); + if (c != null) { + if (c.moveToNext()) { + loadFromCursor(c); // 如果Cursor非空,加载数据 + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); // 查询结果为空时输出警告 + } + } + } finally { + if (c != null) // 释放Cursor资源 + c.close(); + } + } + + // 从Cursor中加载笔记数据到实例属性 + private void loadFromCursor(Cursor c) { + mId = c.getLong(ID_COLUMN); // 获取笔记ID + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); // 获取提醒日期 + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); // 获取背景颜色ID + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); // 获取创建日期 + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); // 获取附件标志 + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); // 获取最后修改日期 + mParentId = c.getLong(PARENT_ID_COLUMN); // 获取父笔记ID + mSnippet = c.getString(SNIPPET_COLUMN); // 获取笔记摘录 + mType = c.getInt(TYPE_COLUMN); // 获取笔记类型 + mWidgetId = c.getInt(WIDGET_ID_COLUMN); // 获取小部件ID + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); // 获取小部件类型 + mVersion = c.getLong(VERSION_COLUMN); // 获取笔记版本号 + } + + /** + * 加载数据内容。 + * 从数据库中查询特定note_id的数据,并将其加载到mDataList中。 + */ + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); // 清空数据列表 + try { + // 查询指定note_id的数据 + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(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"); + return; + } + // 遍历查询结果,并加载到mDataList中 + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); // 创建SqlData对象 + mDataList.add(data); // 添加到数据列表 + } + } else { + // 如果查询结果为null,打印警告信息 + Log.w(TAG, "loadDataContent: cursor = null"); + } + } finally { + // 释放资源 + if (c != null) + c.close(); + } + } + + /** + * 设置内容。 + * 根据传入的JSONObject,更新或创建笔记的相关内容。 + * + * @param js 包含笔记信息的JSONObject。 + * @return 成功返回true,失败返回false。 + */ + public boolean setContent(JSONObject js) { + try { + // 从js中获取note信息 + 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) { + // 文件夹类型笔记,仅更新snippet和类型 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); // 保存snippet的差异 + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); // 保存类型的差异 + } + mType = type; + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + // 笔记类型,更新或设置多种信息 + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); // 保存ID差异 + } + mId = id; + + // 更新或设置提醒日期、背景色id、创建日期、附件标志、修改日期、父id、snippet、类型、小部件id和类型等信息 + // 该部分通过条件判断,确定是否需要更新数据库字段 + + // 处理数据项数组,每个数据项会被更新或创建 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; // 复用现有数据 + } + } + } + + if (sqlData == null) { + sqlData = new SqlData(mContext); // 创建新数据项 + mDataList.add(sqlData); // 添加到列表 + } + + sqlData.setContent(data); // 设置数据项内容 + } + } + } catch (JSONException e) { + // 处理JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; // 解析失败 + } + return true; // 解析成功 + } + + /** + * 获取内容。 + * 将当前笔记的内容转换为JSONObject格式。 + * + * @return 笔记内容的JSONObject,如果无法转换成功则返回null。 + */ + public JSONObject getContent() { + try { + JSONObject js = new JSONObject(); + + if (mIsCreate) { + // 如果笔记尚未在数据库中创建,返回null + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + + JSONObject note = new JSONObject(); + // 根据笔记类型,填充不同的信息到note JSONObject中 + // 该部分通过条件判断,根据mType选择需要填充的信息 + + // 将note和data信息添加到js中 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 添加笔记信息 + // 处理数据项数组,将其添加到js中 + JSONArray dataArray = new JSONArray(); + for (SqlData data : mDataList) { + dataArray.put(data.getContent()); // 获取每个数据项的内容 + } + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); // 添加数据项信息 + + return js; // 返回完整内容 + } catch (JSONException e) { + // 处理JSON构建异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; // 转换失败时返回null + } + + /** + * 设置父id。 + * + * @param id 父笔记的id。 + */ + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); // 保存父ID的差异 + } + + /** + * 设置Gtask id。 + * + * @param gid Gtask的id。 + */ + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); // 保存Gtask ID的差异 + } + + /** + * 设置同步id。 + * + * @param syncId 同步的id。 + */ + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); // 保存同步ID的差异 + } + + /** + * 重置本地修改标志。 + */ + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); // 重置本地修改标志 + } + + /** + * 获取笔记id。 + * + * @return 笔记的id。 + */ + public long getId() { + return mId; // 返回笔记ID + } + + /** + * 获取父id。 + * + * @return 父笔记的id。 + */ + public long getParentId() { + return mParentId; // 返回父ID + } + + /** + * 获取snippet。 + * + * @return 笔记的snippet。 + */ + public String getSnippet() { + return mSnippet; // 返回笔记摘录 + } + + /** + * 判断是否为笔记类型。 + * + * @return 是笔记类型返回true,否则返回false。 + */ + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; // 检查当前类型 + } + + /** + * 提交对笔记的更改或创建新的笔记。 + * + * @param validateVersion 是否验证版本号。如果为 true,则在更新笔记时会检查版本号以避免并发更新的问题。 + * 如果为 false,则不进行版本号检查。 + * 这个参数主要用于处理客户端在同步过程中可能同时更新同一笔记的情况。 + */ +/** + * 提交对笔记的更改或创建新的笔记。 + * + * @param validateVersion 是否验证版本号。如果为 true,则在更新笔记时会检查版本号以避免并发更新的问题。 + * 如果为 false,则不进行版本号检查。 + * 这个参数主要用于处理客户端在同步过程中可能同时更新同一笔记的情况。 + */ +public void commit(boolean validateVersion) { + // 判断当前是否为创建新笔记的状态 + if (mIsCreate) { // 处理创建新笔记的逻辑 + // 在创建新笔记时,如果ID是无效的(即未指定),且包含了ID字段,则移除该字段 + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); // 移除无效ID字段 + } + + // 使用ContentResolver插入新的笔记数据到数据库 + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + + try { + // 从插入返回的URI中解析出新笔记的ID + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); // 记录错误日志 + // 如果无法解析出ID,抛出异常,表示创建失败 + throw new ActionFailureException("create note failed"); + } + // 检查解析出的ID是否有效 + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); // 如果ID无效,抛出异常 + } + + // 如果是创建笔记类型,提交关联数据 + 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, "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) // 使用当前笔记ID + }); + } else { + // 验证版本号时,进行条件更新 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[]{ + String.valueOf(mId), String.valueOf(mVersion) // 检查当前版本号 + }); + } + + // 如果更新结果为0,说明没有进行任何更新,可能是由于同步时用户同时更新了笔记 + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); // 记录警告日志 + } + } + + // 如果是笔记类型,提交关联数据 + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); // 提交每个关联数据项 + } + } + } + + // 刷新本地信息,加载最新数据 + loadFromCursor(mId); // 从数据库重新加载当前笔记信息 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); // 如果是笔记类型,加载笔记关联的数据内容 + + // 清空差异数据,重置创建状态 + mDiffNoteValues.clear(); // 清空差异值 + mIsCreate = false; // 设置当前状态为非创建状态 +} + diff --git a/src/gtask/data/Task.java b/src/gtask/data/Task.java new file mode 100644 index 0000000..ada2d4b --- /dev/null +++ b/src/gtask/data/Task.java @@ -0,0 +1,415 @@ +/* + * 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; + +/** + * 任务类,表示一个待办事项。 + * 用于管理和同步任务数据。 + */ +public class Task extends Node { + // 日志标签 + private static final String TAG = Task.class.getSimpleName(); + + // 任务完成状态 + private boolean mCompleted; + + // 任务备注 + private String mNotes; + + // 任务元信息,包含额外的JSON格式信息 + private JSONObject mMetaInfo; + + // 前一个兄弟任务 + private Task mPriorSibling; + + // 任务所属的任务列表 + private TaskList mParent; + + /** + * 构造函数,初始化任务状态。 + */ + public Task() { + super(); // 调用父类Node的构造函数 + mCompleted = false; // 初始化任务为未完成状态 + mNotes = null; // 初始化备注为null + mPriorSibling = null; // 初始前一个兄弟任务为null + mParent = null; // 初始化父任务列表为null + mMetaInfo = null; // 初始化元信息为null + } + + /** + * 生成创建任务的JSON动作对象。 + * + * @param actionId 动作ID + * @return 包含创建任务动作的JSON对象 + * @throws ActionFailureException 如果生成JSON对象失败 + */ + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置动作类型为创建 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // 设置动作ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务在父列表中的索引 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // 设置任务实体信息 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_TASK); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 将任务实体信息添加到JSON + + // 设置父任务ID + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // 设置目标父类型为任务列表 + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // 设置任务列表ID + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // 如果存在前一个兄弟任务,设置其ID + if (mPriorSibling != null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-create jsonobject"); // 抛出异常,表示JSON生成失败 + } + + return js; // 返回生成的JSON对象 + } + + /** + * 生成更新任务的JSON动作对象。 + * + * @param actionId 动作ID + * @return 包含更新任务动作的JSON对象 + * @throws ActionFailureException 如果生成JSON对象失败 + */ + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置动作类型为更新 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // 设置动作ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务ID + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // 设置任务实体信息 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 设置删除状态 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 将实体信息添加到JSON + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-update jsonobject"); // 抛出异常,表示JSON生成失败 + } + + return js; // 返回生成的JSON对象 + } + + /** + * 根据远程JSON对象设置任务内容。 + * + * @param js 远程获取的JSON对象 + * @throws ActionFailureException 如果从JSON对象中获取内容失败 + */ + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // 从JSON中解析任务信息 + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); // 设置任务ID + } + + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); // 设置最后修改时间 + } + + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); // 设置任务名称 + } + + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); // 设置任务备注 + } + + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); // 设置任务删除状态 + } + + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); // 设置任务完成状态 + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to get task content from jsonobject"); // 抛出异常,表示获取内容失败 + } + } + } + + /** + * 根据本地JSON对象设置任务内容。 + * + * @param js 本地获取的JSON对象 + * @throws ActionFailureException 如果从JSON对象中获取内容失败 + */ + 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 available"); // 记录警告,无内容可设置 + return; + } + + try { + 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; + } + + // 遍历数据数组,设置任务名称 + 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) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + } + } + + /** + * 根据任务内容生成本地JSON对象。 + * + * @return 本地JSON对象,用于数据同步 + */ + 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; // 返回null + } + + JSONObject js = new JSONObject(); // 创建新的JSON对象 + 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); // 添加数据数组到JSON + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 设置笔记类型 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 添加笔记信息到JSON + return js; // 返回生成的JSON对象 + } 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)) { + data.put(DataColumns.CONTENT, getName()); // 更新任务内容 + break; // 找到后退出循环 + } + } + + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 确保笔记信息的类型为任务 + return mMetaInfo; // 返回元信息 + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + return null; // 返回null,表示生成失败 + } + } + + /** + * 设置任务的元信息。 + * + * @param metaData 元数据对象,包含任务的额外信息 + */ + public void setMetaInfo(MetaData metaData) { + if (metaData != null && metaData.getNotes() != null) { + try { + mMetaInfo = new JSONObject(metaData.getNotes()); // 创建元信息的JSON对象 + } catch (JSONException e) { + Log.w(TAG, e.toString()); // 记录警告 + mMetaInfo = null; // 设置为null + } + } + } + + /** + * 根据数据库游标获取同步动作类型。 + * + * @param c 数据库游标,指向当前任务的数据行 + * @return 同步动作类型,定义了任务数据的同步方向 + */ + 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 == null) { + Log.w(TAG, "it seems that note meta has been deleted"); + return SYNC_ACTION_UPDATE_REMOTE; // 如果元信息已删除,返回更新远程标识 + } + + // 检查远程任务ID是否存在 + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + return SYNC_ACTION_UPDATE_LOCAL; // 返回更新本地标识 + } + + // 验证本地和远程任务ID是否匹配 + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); // 记录事件,ID不匹配 + return SYNC_ACTION_UPDATE_LOCAL; + } + + // 检查本地修改状态 + 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 { + // 本地已修改 + 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()) { + // 仅本地修改 + return SYNC_ACTION_UPDATE_REMOTE; + } else { + return SYNC_ACTION_UPDATE_CONFLICT; // 返回更新冲突标识 + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; // 默认返回错误标识 + } + + /** + * 判断任务是否值得保存。 + * + * @return 如果任务有名称或备注,或者有元信息则返回true,否则返回false。 + */ + 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 setNotes(String notes) { + this.mNotes = notes; // 设置任务备注 + } + + public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; // 设置前一个兄弟任务 + } + + public void setParent(TaskList parent) { + this.mParent = parent; // 设置任务列表作为父任务 + } + + public boolean getCompleted() { + return this.mCompleted; // 获取任务完成状态 + } + + public String getNotes() { + return this.mNotes; // 获取任务备注 + } + + public Task getPriorSibling() { + return this.mPriorSibling; // 获取前一个兄弟任务 + } + + public TaskList getParent() { + return this.mParent; // 获取任务列表父对象 + } +} diff --git a/src/gtask/data/TaskList.java b/src/gtask/data/TaskList.java new file mode 100644 index 0000000..015f659 --- /dev/null +++ b/src/gtask/data/TaskList.java @@ -0,0 +1,463 @@ +/* + * 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.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +/** + * 任务列表类,继承自Node类。用于管理一组任务(Task)对象。 + */ +public class TaskList extends Node { + // 日志标签 + private static final String TAG = TaskList.class.getSimpleName(); + + // 列表中的任务索引 + private int mIndex; + + // 存储子任务的列表 + private ArrayList mChildren; + + /** + * 构造函数,初始化任务列表。 + */ + public TaskList() { + super(); // 调用父类Node构造函数 + mChildren = new ArrayList(); // 初始化子任务列表 + mIndex = 1; // 默认索引设置为1 + } + + /** + * 生成创建任务列表的动作JSON对象。 + * + * @param actionId 动作标识符 + * @return 包含创建任务列表动作的JSON对象 + * @throws ActionFailureException 如果生成JSON对象失败,则抛出异常 + */ + public JSONObject getCreateAction(int actionId) throws ActionFailureException { + JSONObject js = new JSONObject(); + + try { + // 设置动作类型为创建 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // 设置动作标识符 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置索引 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // 设置实体变化信息 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 将任务列表实体信息加入JSON + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); // 抛出异常,表示JSON生成失败 + } + + return js; // 返回生成的JSON对象 + } + + /** + * 生成更新任务列表的动作JSON对象。 + * + * @param actionId 动作标识符 + * @return 包含更新任务列表动作的JSON对象 + * @throws ActionFailureException 如果生成JSON对象失败,则抛出异常 + */ + public JSONObject getUpdateAction(int actionId) throws ActionFailureException { + JSONObject js = new JSONObject(); + + try { + // 设置动作类型为更新 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // 设置动作标识符 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务列表ID + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // 设置实体变化信息 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 设置删除状态 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 将实体信息加入JSON + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); // 抛出异常,表示JSON生成失败 + } + + return js; // 返回生成的JSON对象 + } + + /** + * 根据远程JSON对象设置任务列表的内容。 + * + * @param js 远程获取的JSON对象 + * @throws ActionFailureException 如果从JSON对象中获取内容失败,则抛出异常 + */ + public void setContentByRemoteJSON(JSONObject js) throws ActionFailureException { + if (js != null) { + try { + // 设置ID + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); // 设置任务列表ID + } + + // 设置最后修改时间 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // 设置名称 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); // 设置任务列表名称 + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to get tasklist content from jsonobject"); // 抛出异常,表示获取内容失败 + } + } + } + + /** + * 根据本地JSON对象设置任务列表的内容。 + * + * @param js 本地获取的JSON对象 + * @throws ActionFailureException 如果从JSON对象中获取内容失败,则抛出异常 + */ + public void setContentByLocalJSON(JSONObject js) throws ActionFailureException { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: nothing is available"); // 记录警告,无有效内容 + return; + } + + try { + JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); // 获取任务列表信息 + + // 根据类型设置任务列表名称 + if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + String name = folder.getString(NoteColumns.SNIPPET); + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); // 任务列表名称加前缀 + } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); // 默认根文件夹名称 + else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE); // 通话记录文件夹名称 + else + Log.e(TAG, "invalid system folder"); // 记录错误,系统文件夹无效 + } else { + Log.e(TAG, "error type"); // 记录错误,类型错误 + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + throw new ActionFailureException("fail to set tasklist content from local json object"); // 抛出异常,表示设置内容失败 + } + } + + /** + * 从任务列表内容生成本地JSON对象。 + * + * @return 本地JSON对象代表的任务列表内容 + */ + public JSONObject getLocalJSONFromContent() { + try { + JSONObject js = new JSONObject(); // 创建新的JSON对象 + 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); // 添加任务列表信息到JSON + + return js; // 返回生成的JSON对象 + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + return null; // 生成失败,返回null + } + } + + /** + * 根据本地数据库游标确定同步动作。 + * + * @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 doesn't match"); // 记录错误,ID不匹配 + return SYNC_ACTION_ERROR; // 返回错误状态 + } + // 对于文件夹情况,应该的返回逻辑 + return SYNC_ACTION_UPDATE_REMOTE; // 仅本地有修改 + } + } catch (Exception e) { + Log.e(TAG, e.toString()); // 记录错误日志 + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; // 默认返回错误状态 + } + + /** + * 获取子任务数量。 + * + * @return 子任务数量 + */ + public int getChildTaskCount() { + return mChildren.size(); // 返回当前子任务的数量 + } + + /** + * 添加一个子任务到列表中。 + * + * @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; // 返回添加结果 + } + + /** + * 在指定索引位置添加一个子任务。 + * + * @param task 要添加的子任务 + * @param index 子任务要插入的索引位置 + * @return 如果添加成功返回true,否则返回false + */ + public boolean addChildTask(Task task, int index) { + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "add child task: invalid index"); // 记录错误,索引无效 + return false; + } + + 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); // 设置后置兄弟 + } + + return true; // 添加成功 + } + + /** + * 从列表中移除一个子任务。 + * + * @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)); // 更新后面任务的前置兄弟 + } + } + } + return ret; // 返回移除结果 + } + + /** + * 移动子任务到指定索引位置。 + * + * @param task 要移动的子任务 + * @param index 子任务要移动到的索引位置 + * @return 如果移动成功返回true,否则返回false + */ + public boolean moveChildTask(Task task, int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "move child task: invalid index"); // 记录错误,索引无效 + return false; + } + + int pos = mChildren.indexOf(task); + if (pos == -1) { + Log.e(TAG, "move child task: the task should in the list"); // 记录错误,任务不在列表中 + return false; + } + + if (pos == index) + return true; // 位置未变,返回成功 + return (removeChildTask(task) && addChildTask(task, index)); // 移除后再添加 + } + + /** + * 根据全局标识符(gid)查找子任务。 + * + * @param gid 要查找的子任务的全局标识符 + * @return 如果找到匹配的子任务,则返回该任务对象;否则返回null。 + */ + public Task findChildTaskByGid(String gid) { + // 遍历子任务列表,查找gid匹配的子任务 + for (int i = 0; i < mChildren.size(); i++) { + Task t = mChildren.get(i); + if (t.getGid().equals(gid)) { + return t; // 返回找到的任务 + } + } + return null; // 未找到则返回null + } + + /** + * 获取指定子任务在列表中的索引位置。 + * + * @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, "getTaskByIndex: invalid index"); // 记录错误,索引无效 + return null; + } + return mChildren.get(index); // 返回子任务 + } + + /** + * 通过遍历子任务列表,查找并返回匹配指定gid的子任务。 + * + * @param gid 要查找的子任务的全局标识符 + * @return 如果找到匹配的子任务,则返回该任务对象;否则返回null。 + */ + public Task getChildTaskByGid(String gid) { + // 遍历子任务列表,查找gid匹配的子任务 + for (Task task : mChildren) { + if (task.getGid().equals(gid)) + return task; // 返回找到的任务 + } + return null; // 未找到则返回null + } + + /** + * 获取所有子任务的列表。 + * + * @return 子任务列表,作为一个ArrayList返回。 + */ + public ArrayList getChildTaskList() { + // 返回存储子任务的列表 + return this.mChildren; + } + + /** + * 设置当前任务的索引。 + * + * @param index 要设置的索引值。 + */ + public void setIndex(int index) { + this.mIndex = index; // 设置任务列表索引 + } + + /** + * 获取当前任务的索引。 + * + * @return 当前任务的索引值。 + */ + public int getIndex() { + return this.mIndex; // 返回当前任务索引 + } +} diff --git a/src/gtask/exception/ActionFailureException.java b/src/gtask/exception/ActionFailureException.java new file mode 100644 index 0000000..3737065 --- /dev/null +++ b/src/gtask/exception/ActionFailureException.java @@ -0,0 +1,53 @@ +/* + * ActionFailureException 类的注释 + * + * 该异常类是运行时异常的子类,用于表示操作失败的异常情况。 + * 它可以包含一个错误消息和导致异常的 Throwable 对象。 + * 这个类主要是为了处理任务或动作执行失败的情况, + * 提供了一个通用的方式来报告和处理这类错误。 + * + * 许可证信息: 见类文件头部的版权声明 + */ + +package net.micode.notes.gtask.exception; + +// 引入 Java 运行时异常类 +import java.lang.RuntimeException; + +/** + * ActionFailureException 类定义了一个操作失败时抛出的异常。 + * + * 这个异常类可以用来表示在执行某个操作过程中出现的故障, + * 比如在任务创建、更新或删除时发生错误。 + */ +public class ActionFailureException extends RuntimeException { + private static final long serialVersionUID = 4425249765923293627L; // 序列化 ID + + /** + * 无参构造函数,创建一个不带详细消息的动作失败异常实例。 + */ + public ActionFailureException() { + super(); // 调用父类RuntimeException的无参构造函数 + } + + /** + * 带有详细信息的构造函数,创建一个带有详细错误消息的动作失败异常实例。 + * + * @param paramString 错误信息字符串,用于描述异常的详细情况。 + */ + public ActionFailureException(String paramString) { + super(paramString); // 调用父类RuntimeException的构造函数,将错误消息传递给它 + } + + /** + * 带有详细信息和导致异常的原因的构造函数,创建一个带有详细错误消息 + * 和导致异常的 Throwable 对象的动作失败异常实例。 + * + * @param paramString 错误信息字符串,用于描述异常的详细情况。 + * @param paramThrowable 导致异常的 Throwable 对象。 + */ + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); // 调用父类RuntimeException的构造函数 + // 将错误信息和抛出的异常传递给父类 + } +} diff --git a/src/gtask/exception/NetworkFailureException.java b/src/gtask/exception/NetworkFailureException.java new file mode 100644 index 0000000..0a488c0 --- /dev/null +++ b/src/gtask/exception/NetworkFailureException.java @@ -0,0 +1,47 @@ +/* + * NetworkFailureException 类的注释 + * + * 该异常类用于表示网络操作失败的异常。它是 Exception 的子类, + * 可用来捕获和处理应用程序中发生的网络错误。 + * 可以通过不同的构造函数来创建包含详细信息或不包含详细信息的 + * NetworkFailureException 实例。 + */ + +package net.micode.notes.gtask.exception; + +/** + * NetworkFailureException 类表示在网络操作中发生的错误。 + * + * 该异常可以用于捕获与网络相关的错误,例如连接失败, + * 超时或数据传输过程中的错误等。 + */ +public class NetworkFailureException extends Exception { + private static final long serialVersionUID = 2107610287180234136L; // 序列化 ID + + /** + * 无参构造函数,用于创建一个不带详细信息的 NetworkFailureException 实例。 + */ + public NetworkFailureException() { + super(); // 调用父类 Exception 的无参构造函数 + } + + /** + * 带有详细信息的构造函数,用于创建一个包含错误信息的 NetworkFailureException 实例。 + * + * @param paramString 错误信息字符串,用于描述异常的详细情况。 + */ + public NetworkFailureException(String paramString) { + super(paramString); // 调用父类的构造函数,将错误信息传递 + } + + /** + * 带有详细信息和导致异常的 Throwable 对象的构造函数, + * 用于创建包含错误信息和原因的 NetworkFailureException 实例。 + * + * @param paramString 错误信息字符串,用于描述异常的详细情况。 + * @param paramThrowable 导致异常的 Throwable 对象。 + */ + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); // 调用父类构造函数,将错误信息和原因传递 + } +} diff --git a/src/gtask/remote/GTaskASyncTask.java b/src/gtask/remote/GTaskASyncTask.java new file mode 100644 index 0000000..54d3cc2 --- /dev/null +++ b/src/gtask/remote/GTaskASyncTask.java @@ -0,0 +1,152 @@ +/* + * GTaskASyncTask 类说明: + * 这是一个继承自AsyncTask的类,用于在后台执行Google任务同步操作。 + * 它可以在一个独立的线程中执行同步任务,并通过通知栏通知用户同步的状态(成功、失败、取消等)。 + * 同时,它提供了接口供调用者监听同步任务的完成。 + */ + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; +import net.micode.notes.ui.NotesPreferenceActivity; + +/** + * GTaskASyncTask 类负责执行 Google 任务的同步操作。 + * 它是一个 AsyncTask,它在后台线程中处理同步任务并更新UI。 + */ +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; // Google任务管理器,用于执行实际的同步操作 + private OnCompleteListener mOnCompleteListener; // 同步完成的监听器 + + /* + * 构造函数 + * @param context 应用的上下文环境 + * @param listener 同步完成时的监听器 + */ + public GTaskASyncTask(Context context, OnCompleteListener listener) { + mContext = context; // 保存上下文 + mOnCompleteListener = listener; // 设置监听器 + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); // 获取通知管理器 + mTaskManager = GTaskManager.getInstance(); // 获取 Google 任务管理器实例 + } + + // 取消同步操作的方法 + public void cancelSync() { + mTaskManager.cancelSync(); // 调用 Google 任务管理器的取消同步方法 + } + + // 发布进度更新的方法 + public void publishProgess(String message) { + publishProgress(new String[]{ message }); // 调用AsyncTask的publishProgress方法 + } + + /* + * 显示通知的方法 + * @param tickerId 通知的Ticker文本资源ID + * @param content 通知的内容文本 + */ + private void showNotification(int tickerId, String content) { + PendingIntent pendingIntent; + + // 根据不同的通知状态设置不同的Intent + if (tickerId != R.string.ticker_success) { + // 如果不是成功状态,跳转到设置界面 + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + } else { + // 如果是成功状态,跳转到任务列表界面 + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + + // 构建通知并显示 + Notification.Builder builder = new Notification.Builder(mContext) + .setAutoCancel(true) // 点击后自动消失 + .setContentTitle(mContext.getString(R.string.app_name)) // 通知标题 + .setContentText(content) // 通知内容 + .setContentIntent(pendingIntent) // 点击按钮处理 + .setWhen(System.currentTimeMillis()) // 通知时间 + .setOngoing(true); // 设定为进行中的通知 + Notification notification = builder.getNotification(); // 构建通知对象 + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); // 显示通知 + } + + /* + * 在后台执行同步操作的方法 + * @return 同步操作的状态码 + */ + @Override + protected Integer doInBackground(Void... unused) { + // 开始同步时的进度更新 + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); // 发布同步中的消息 + return mTaskManager.sync(mContext, this); // 执行同步操作,并返回状态码 + } + + /* + * 更新进度的方法,会在调用publishProgress后被调用 + * @param progress 进度更新的内容 + */ + @Override + protected void onProgressUpdate(String... progress) { + // 显示当前同步进度 + showNotification(R.string.ticker_syncing, progress[0]); // 显示同步进度的通知 + // 如果上下文是一个GTaskSyncService实例,发送广播更新进度 + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); // 发送进度广播 + } + } + + /* + * 同步任务完成后的处理方法 + * @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())); + 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)); + } + // 如果设置了完成监听器,则在一个新线程中调用其onComplete方法 + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + public void run() { + mOnCompleteListener.onComplete(); // 调用监听器的完成方法 + } + }).start(); + } + } +} diff --git a/src/gtask/remote/GTaskClient.java b/src/gtask/remote/GTaskClient.java new file mode 100644 index 0000000..e83cbfc --- /dev/null +++ b/src/gtask/remote/GTaskClient.java @@ -0,0 +1,1234 @@ +/* + * 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; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +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.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +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; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + + +/** + * GTaskClient类用于与Google任务服务进行远程交互。 + * 提供了登录、获取任务列表、添加任务等操作的方法。 + */ +public class GTaskClient { + // 日志标签 + private static final String TAG = GTaskClient.class.getSimpleName(); + + // Google任务服务的基础URL + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + // 用于获取任务信息的URL + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + // 用于提交任务信息的URL + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + // 单例模式实例 + private static GTaskClient mInstance = null; + + // HTTP客户端 + private DefaultHttpClient mHttpClient; + + // GET请求URL + private String mGetUrl; + + // POST请求URL + private String mPostUrl; + + // 客户端版本号 + private long mClientVersion; + + // 是否已登录 + private boolean mLoggedin; + + // 最后登录时间 + private long mLastLoginTime; + + // 操作ID,用于标识一次操作 + private int mActionId; + + // 用户账户信息 + private Account mAccount; + + // 用于存储更新数据的JSON数组 + private JSONArray mUpdateArray; + + /** + * GTaskClient的私有构造方法,初始化各种属性。 + */ + private GTaskClient() { + // 初始化客户端 + mHttpClient = null; + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; + mLoggedin = false; + mLastLoginTime = 0; + mActionId = 1; + mAccount = null; + mUpdateArray = null; + } + + /** + * 获取GTaskClient的单例实例。 + * + * @return GTaskClient的单例实例。 + */ + public static synchronized GTaskClient getInstance() { + // 确保仅创建一个实例 + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + /** + * 用户登录函数。 + * + * @param activity 当前活动,用于获取账户信息和上下文。 + * @return 登录成功返回true,失败返回false。 + */ + public boolean login(Activity activity) { + // 检查登录是否过期 + final long interval = 1000 * 60 * 5; // 5分钟 + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // 检查账户是否切换,需要重新登录 + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + + // 如果已经登录,则直接返回成功 + if (mLoggedin) { + Log.d(TAG, "already logged in"); + return true; + } + + // 记录当前登录时间 + mLastLoginTime = System.currentTimeMillis(); + // 尝试登录Google账户 + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // 如果是自定义域名邮箱,则尝试使用自定义域名登录 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + // 构造自定义域名的登录URL + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + // 尝试使用自定义域名登录 + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // 如果使用自定义域名登录失败,则尝试使用官方URL登录 + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + + // 登录成功 + mLoggedin = true; + return true; + } + + + /** + * 使用Google账户登录,获取授权令牌。 + * + * @param activity 当前活动,用于获取账户管理器。 + * @param invalidateToken 是否吊销之前的令牌并重新获取。 + * @return 返回获取到的授权令牌,如果失败或没有可用账户返回null。 + */ + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + // 获取账户管理器和所有Google账户 + AccountManager accountManager = AccountManager.get(activity); + Account[] accounts = accountManager.getAccountsByType("com.google"); + + // 检查是否有可用的Google账户 + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + + // 根据设置中的账户名选择账户 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + // 检查是否找到设置中对应的账户 + if (account != null) { + mAccount = account; + } else { + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // 获取授权令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + Bundle authTokenBundle = accountManagerFuture.getResult(); + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 如果需要,吊销令牌并重新获取 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; + } + + /** + * 尝试使用授权令牌登录Gtask。 + * + * @param activity 当前活动,用于登录过程中的UI交互。 + * @param authToken 授权令牌。 + * @return 如果登录成功返回true,否则返回false。 + */ + private boolean tryToLoginGtask(Activity activity, String authToken) { + // 首次尝试登录Gtask + if (!loginGtask(authToken)) { + // 如果失败,尝试吊销令牌并重新获取后再次登录 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // 使用新令牌再次尝试登录Gtask + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } + + /** + * 执行Gtask登录操作。 + * + * @param authToken 授权令牌。 + * @return 登录成功返回true,失败返回false。 + */ + private boolean loginGtask(String authToken) { + // 设置HTTP连接参数 + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + mHttpClient = new DefaultHttpClient(httpParameters); + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // 使用授权令牌登录Gtask + try { + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // 检查是否获取到授权Cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // 解析响应,获取客户端版本 + 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); + } + JSONObject js = new JSONObject(jsString); + mClientVersion = js.getLong("v"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } catch (Exception e) { + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } + + + /** + * 获取一个唯一的动作ID + * + * @return 返回当前动作的ID,每次调用自增 + */ + private int getActionId() { + return mActionId++; + } + + /** + * 创建一个HttpPost请求 + * + * @return 配置好的HttpPost对象 + */ + private HttpPost createHttpPost() { + HttpPost httpPost = new HttpPost(mPostUrl); + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + return httpPost; + } + + /** + * 从HttpEntity中获取响应内容 + * + * @param entity Http响应实体 + * @return 响应内容的字符串 + * @throws IOException 当读取响应内容失败时抛出 + */ + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); + Log.d(TAG, "encoding: " + contentEncoding); + } + + InputStream input = entity.getContent(); + // 根据内容编码类型,对输入流进行解压 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + InputStreamReader isr = new InputStreamReader(input); + BufferedReader br = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + + // 读取并构建响应内容字符串 + while (true) { + String buff = br.readLine(); + if (buff == null) { + return sb.toString(); + } + sb = sb.append(buff); + } + } finally { + input.close(); + } + } + + /** + * 发送POST请求,并返回解析后的JSONObject + * + * @param js 要发送的JSON对象 + * @return 请求响应的JSONObject + * @throws NetworkFailureException 当网络请求或处理失败时抛出 + */ + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + HttpPost httpPost = createHttpPost(); + try { + LinkedList list = new LinkedList(); + list.add(new BasicNameValuePair("r", js.toString())); + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entity); + + // 执行POST请求 + HttpResponse response = mHttpClient.execute(httpPost); + String jsString = getResponseContent(response.getEntity()); + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("unable to convert response content to jsonobject"); + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("error occurs when posting request"); + } + } + + /** + * 创建一个任务 + * + * @param task 要创建的任务对象 + * @throws NetworkFailureException 当网络操作失败时抛出 + */ + public void createTask(Task task) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // 构建动作列表 + actionList.put(task.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // 添加客户端版本信息 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // 发送请求并处理响应 + JSONObject jsResponse = postRequest(jsPost); + 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: handling jsonobject failed"); + } + } + + + /** + * 创建任务列表。 + * + * @param tasklist 任务列表对象,包含创建任务所需的信息。 + * @throws NetworkFailureException 网络请求失败时抛出。 + */ + /* + * 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; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +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.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +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; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * GTaskClient 类用于与 Google 任务服务进行远程交互。 + * 提供了登录、获取任务列表、添加任务等操作的方法。 + */ +public class GTaskClient { + // 日志标签 + private static final String TAG = GTaskClient.class.getSimpleName(); + + // Google 任务服务的基础 URL + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + // 获取任务信息的 URL + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + // 提交任务信息的 URL + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + // 单例模式实例 + private static GTaskClient mInstance = null; + + // HTTP 客户端 + private DefaultHttpClient mHttpClient; + + // GET 请求 URL + private String mGetUrl; + + // POST 请求 URL + private String mPostUrl; + + // 客户端版本号 + private long mClientVersion; + + // 是否已登录 + private boolean mLoggedin; + + // 最后登录时间 + private long mLastLoginTime; + + // 操作 ID,用于标识一次操作 + private int mActionId; + + // 用户账户信息 + private Account mAccount; + + // 用于存储更新数据的 JSON 数组 + private JSONArray mUpdateArray; + + /** + * GTaskClient 的私有构造方法,初始化各种属性。 + */ + private GTaskClient() { + // 初始化客户端 + mHttpClient = null; + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; + mLoggedin = false; + mLastLoginTime = 0; + mActionId = 1; + mAccount = null; + mUpdateArray = null; + } + + /** + * 获取 GTaskClient 的单例实例。 + * + * @return GTaskClient 的单例实例。 + */ + public static synchronized GTaskClient getInstance() { + // 确保仅创建一个实例 + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + /** + * 用户登录函数。 + * + * @param activity 当前活动,用于获取账户信息和上下文。 + * @return 登录成功返回 true,失败返回 false。 + */ + public boolean login(Activity activity) { + // 检查登录是否过期 + final long interval = 1000 * 60 * 5; // 5分钟 + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; // 登录超时 + } + + // 检查账户是否切换,需要重新登录 + if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity.getSyncAccountName(activity))) { + mLoggedin = false; // 切换账户,重新登录 + } + + // 如果已经登录,则直接返回成功 + if (mLoggedin) { + Log.d(TAG, "already logged in"); + return true; + } + + // 记录当前登录时间 + mLastLoginTime = System.currentTimeMillis(); + + // 尝试登录 Google 账户 + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // 如果是自定义域名邮箱,则尝试使用自定义域名登录 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase().endsWith("googlemail.com"))) { + // 构造自定义域名的登录 URL + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + // 尝试使用自定义域名登录 + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; // 登录成功 + } + } + + // 如果使用自定义域名登录失败,则尝试使用官方 URL 登录 + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; // 登录失败 + } + } + + // 登录成功 + mLoggedin = true; + return true; + } + + /** + * 使用 Google 账户登录,获取授权令牌。 + * + * @param activity 当前活动,用于获取账户管理器。 + * @param invalidateToken 是否吊销之前的令牌并重新获取。 + * @return 返回获取到的授权令牌,如果失败或没有可用账户返回null。 + */ + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + // 获取账户管理器和所有 Google 账户 + AccountManager accountManager = AccountManager.get(activity); + Account[] accounts = accountManager.getAccountsByType("com.google"); + + // 检查是否有可用的 Google 账户 + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; // 无可用账户 + } + + // 根据设置中的账户名选择账户 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; // 找到账户 + break; + } + } + // 检查是否找到设置中对应的账户 + if (account != null) { + mAccount = account; // 保存账户信息 + } else { + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; // 找不到账户 + } + + // 获取授权令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + Bundle authTokenBundle = accountManagerFuture.getResult(); + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 如果需要,吊销令牌并重新获取 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); // 重新登录 + } + } catch (Exception e) { + Log.e(TAG, "get auth token failed"); + authToken = null; // 获取令牌失败 + } + + return authToken; // 返回授权令牌 + } + + /** + * 尝试使用授权令牌登录 Gtask。 + * + * @param activity 当前活动,用于登录过程中的 UI 交互。 + * @param authToken 授权令牌。 + * @return 如果登录成功返回 true,否则返回 false。 + */ + private boolean tryToLoginGtask(Activity activity, String authToken) { + // 首次尝试登录 Gtask + if (!loginGtask(authToken)) { + // 如果失败,尝试吊销令牌并重新获取后再次登录 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; // 登录失败 + } + + // 使用新令牌再次尝试登录 Gtask + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; // 登录失败 + } + } + return true; // 登录成功 + } + + /** + * 执行 Gtask 登录操作。 + * + * @param authToken 授权令牌。 + * @return 登录成功返回 true,失败返回 false。 + */ + private boolean loginGtask(String authToken) { + // 设置 HTTP 连接参数 + int timeoutConnection = 10000; // 连接超时 + int timeoutSocket = 15000; // 套接字超时 + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + mHttpClient = new DefaultHttpClient(httpParameters); // 初始化 HTTP 客户端 + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // 使用授权令牌登录 Gtask + try { + String loginUrl = mGetUrl + "?auth=" + authToken; // 构造登录 URL + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = mHttpClient.execute(httpGet); // 执行 GET 请求 + + // 检查是否获取到授权 Cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; // 找到授权 Cookie + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); // 没有找到授权 cookie + } + + // 解析响应,获取客户端版本 + 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); + } + JSONObject js = new JSONObject(jsString); + mClientVersion = js.getLong("v"); // 获取客户端版本 + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; // 解析 JSON 失败 + } catch (Exception e) { + Log.e(TAG, "httpget gtask_url failed"); + return false; // HTTP 请求失败 + } + + return true; // 登录成功 + } + + /** + * 获取一个唯一的动作 ID + * + * @return 返回当前动作的 ID,每次调用自增 + */ + private int getActionId() { + return mActionId++; + } + + /** + * 创建一个 HttpPost 请求 + * + * @return 配置好的 HttpPost 对象 + */ + private HttpPost createHttpPost() { + HttpPost httpPost = new HttpPost(mPostUrl); // 创建 POST 请求 + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); // 设置请求头 + httpPost.setHeader("AT", "1"); // 设置其他请求头 + return httpPost; + } + + /** + * 从 HttpEntity 中获取响应内容 + * + * @param entity Http 响应实体 + * @return 响应内容的字符串 + * @throws IOException 当读取响应内容失败时抛出 + */ + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; // 内容编码 + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); // 获取 encoding + Log.d(TAG, "encoding: " + contentEncoding); + } + + InputStream input = entity.getContent(); // 获取输入流 + // 根据内容编码类型,对输入流进行解压 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + InputStreamReader isr = new InputStreamReader(input); + BufferedReader br = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + + // 读取并构建响应内容字符串 + while (true) { + String buff = br.readLine(); + if (buff == null) { + return sb.toString(); // 返回完整的字符串 + } + sb.append(buff); // 添加每一行 + } + } finally { + input.close(); // 关闭输入流 + } + } + + /** + * 发送 POST 请求,并返回解析后的 JSONObject + * + * @param js 要发送的 JSON 对象 + * @return 请求响应的 JSONObject + * @throws NetworkFailureException 当网络请求或处理失败时抛出 + */ + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); // 确保已登录 + } + + HttpPost httpPost = createHttpPost(); // 创建 POST 请求 + try { + LinkedList list = new LinkedList(); + list.add(new BasicNameValuePair("r", js.toString())); // 添加请求参数 + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); // 设置请求实体 + httpPost.setEntity(entity); // 将实体添加到请求中 + + // 执行 POST 请求 + HttpResponse response = mHttpClient.execute(httpPost); + String jsString = getResponseContent(response.getEntity()); // 读取响应内容 + return new JSONObject(jsString); // 返回 JSON 对象回复 + + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); // 网络请求失败 + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); // 网络请求失败 + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("unable to convert response content to jsonobject"); // JSON 解析失败 + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("error occurs when posting request"); // 其他错误 + } + } + + /** + * 创建一个任务 + * + * @param task 要创建的任务对象 + * @throws NetworkFailureException 当网络操作失败时抛出 + */ + public void createTask(Task task) throws NetworkFailureException { + commitUpdate(); // 提交更新 + try { + JSONObject jsPost = new JSONObject(); // 创建 POST 请求的 JSON 对象 + JSONArray actionList = new JSONArray(); // 动作列表 + + // 构建动作列表 + actionList.put(task.getCreateAction(getActionId())); // 添加创建任务的动作 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // 添加客户端版本信息 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // 发送请求并处理响应 + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); // 设置新任务的 ID + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handling jsonobject failed"); // JSON 处理失败 + } + } + + /** + * 创建任务列表。 + * + * @param tasklist 任务列表对象,包含创建任务所需的信息。 + * @throws NetworkFailureException 网络请求失败时抛出。 + */ +public void createTaskList(TaskList tasklist) throws NetworkFailureException { + commitUpdate(); // 提交更新,确保当前更新已处理 + + try { + JSONObject jsPost = new JSONObject(); // 创建POST请求的JSON对象 + JSONArray actionList = new JSONArray(); // 动作列表 + + // 添加创建任务的动作到动作列表 + actionList.put(tasklist.getCreateAction(getActionId())); // 获取任务列表创建的动作并添加 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // 将动作列表放入JSON对象 + + // 添加客户端版本信息 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // 发送POST请求并处理响应 + JSONObject jsResponse = postRequest(jsPost); // 执行网络请求 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); // 获取响应结果的第一个元素 + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); // 设置任务列表的全局唯一ID + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录异常 + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handling JSON object failed"); // 处理JSON时失败 + } +} + +/** + * 提交待更新的任务信息。 + * + * 将存储在更新数组中的所有待处理变更发送到服务器。 + * + * @throws NetworkFailureException 网络请求失败时抛出。 + */ +public void commitUpdate() throws NetworkFailureException { + if (mUpdateArray != null) { // 检查更新数组是否为空 + try { + JSONObject jsPost = new JSONObject(); // 创建POST请求的JSON对象 + + // 添加更新的动作列表 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + + // 添加客户端版本信息 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); // 发送POST请求 + mUpdateArray = null; // 清空更新数组 + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录异常 + e.printStackTrace(); + throw new ActionFailureException("commit update: handling JSON object failed"); // 处理JSON时失败 + } + } +} + +/** + * 添加一个待更新的任务节点。 + * + * 将提供的任务节点信息添加到待更新的任务列表中。 + * + * @param node 待添加的节点信息。 + * @throws NetworkFailureException 网络请求失败时抛出。 + */ +public void addUpdateNode(Node node) throws NetworkFailureException { + if (node != null) { // 检查节点是否为空 + // 若更新节点过多,则提交当前更新 + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate(); // 提交当前更新,避免更新过多 + } + + if (mUpdateArray == null) + mUpdateArray = new JSONArray(); // 创建更新节点的数组 + mUpdateArray.put(node.getUpdateAction(getActionId())); // 添加节点更新动作 + } +} + +/** + * 移动任务到不同的任务列表或在同一任务列表内移动位置。 + * + * @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(); // 创建POST请求的JSON对象 + JSONArray actionList = new JSONArray(); // 动作列表 + JSONObject action = new JSONObject(); // 单个动作 + + // 添加移动任务的动作 + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); // 设置移动类型 + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); // 设置动作ID + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); // 设置要移动任务的ID + + if (preParent == curParent && task.getPriorSibling() != null) { + // 如果在同一任务列表内移动且不是第一个任务,则添加前置兄弟节点ID + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } + + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); // 设置原任务列表ID + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); // 设置新父任务列表ID + + if (preParent != curParent) { + // 如果跨任务列表移动,添加目标任务列表ID + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + + actionList.put(action); // 将动作添加到列表 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // 添加动作列表到JSON对象 + + // 添加客户端版本信息 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); // 发送POST请求 + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录异常 + e.printStackTrace(); + throw new ActionFailureException("move task: handling JSON object failed"); // 处理JSON时失败 + } +} + +/** + * 删除指定的任务节点。 + * + * @param node 要删除的节点。 + * @throws NetworkFailureException 网络请求失败时抛出。 + */ +public void deleteNode(Node node) throws NetworkFailureException { + commitUpdate(); // 提交当前更新 + + try { + JSONObject jsPost = new JSONObject(); // 创建POST请求的JSON对象 + JSONArray actionList = new JSONArray(); // 动作列表 + + // 添加删除节点的动作 + node.setDeleted(true); // 设置节点为删除状态 + actionList.put(node.getUpdateAction(getActionId())); // 获取更新动作并添加 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // 添加动作列表 + + // 添加客户端版本信息 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); // 发送POST请求 + mUpdateArray = null; // 清空更新数组 + + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录异常 + e.printStackTrace(); + throw new ActionFailureException("delete node: handling JSON object failed"); // 处理JSON时失败 + } +} + +/** + * 获取任务列表的网络请求。 + * 注意:调用此方法前需要确保用户已经登录。 + * + * @return JSONArray 返回一个包含任务列表的JSON数组。 + * @throws NetworkFailureException 如果网络请求失败则抛出此异常。 + */ +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); // 创建GET请求 + HttpResponse response = mHttpClient.execute(httpGet); // 执行请求 + + // 从响应中提取任务列表 + 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); // 提取JSON字符串 + } + JSONObject js = new JSONObject(jsString); + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); // 返回任务列表的JSON数组 + } 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: handling JSON object failed"); // JSON处理失败 + } +} + +/** + * 根据列表ID获取特定任务列表的网络请求。 + * + * @param listGid 列表的全局唯一标识符。 + * @return JSONArray 返回一个包含特定任务列表的JSON数组。 + * @throws NetworkFailureException 如果网络请求失败则抛出此异常。 + */ +public JSONArray getTaskList(String listGid) throws NetworkFailureException { + commitUpdate(); // 提交当前更新 + try { + JSONObject jsPost = new JSONObject(); // 创建POST请求的JSON对象 + 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); // 添加客户端版本信息 + JSONObject jsResponse = postRequest(jsPost); // 发送POST请求 + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); // 返回任务的JSON数组 + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录异常 + e.printStackTrace(); + throw new ActionFailureException("get task list: handling JSON object failed"); // JSON处理失败 + } +} + +/** + * 获取同步账户信息。 + * + * @return Account 返回当前的同步账户。 + */ +public Account getSyncAccount() { + return mAccount; // 返回同步账户信息 +} + +/** + * 重置更新数组。 + * 用于在进行新的同步之前清空或重置更新的数据数组。 + */ +public void resetUpdateArray() { + mUpdateArray = null; // 清空更新数组 +} + + diff --git a/src/gtask/remote/GTaskManager.java b/src/gtask/remote/GTaskManager.java new file mode 100644 index 0000000..82b7992 --- /dev/null +++ b/src/gtask/remote/GTaskManager.java @@ -0,0 +1,948 @@ +/* + * 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.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.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.tool.DataUtils; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + + +public class GTaskManager { + // 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; + + // GTaskManager的单例实例。 + private static GTaskManager mInstance = null; + + // 关联的Activity对象。 + private Activity mActivity; + + // 上下文对象。 + private Context mContext; + + // 内容解析器。 + private ContentResolver mContentResolver; + + // 标记是否正在同步。 + private boolean mSyncing; + + // 标记是否已取消同步。 + private boolean mCancelled; + + // 保存任务列表的HashMap,键为列表ID,值为任务列表对象。 + private HashMap mGTaskListHashMap; + + // 保存任务的HashMap,键为任务ID,值为任务对象。 + private HashMap mGTaskHashMap; + + // 保存元数据的HashMap,键为元数据ID,值为元数据对象。 + private HashMap mMetaHashMap; + + // 元数据列表。 + private TaskList mMetaList; + + // 本地删除任务ID的集合。 + private HashSet mLocalDeleteIdMap; + + // 保存任务全局ID到本地ID的映射的HashMap。 + private HashMap mGidToNid; + + // 保存本地ID到任务全局ID的映射的HashMap。 + private HashMap mNidToGid; + + // GTaskManager的私有构造函数,初始化各种状态和映射。 + private GTaskManager() { + mSyncing = false; + mCancelled = false; + mGTaskListHashMap = new HashMap(); + mGTaskHashMap = new HashMap(); + mMetaHashMap = new HashMap(); + mMetaList = null; + mLocalDeleteIdMap = new HashSet(); + mGidToNid = new HashMap(); + mNidToGid = new HashMap(); + } + + + /** + * 获取 GTaskManager 的单例对象。 + * 采用单例模式确保全局仅有一个 GTaskManager 实例。 + * + * @return GTaskManager 的单例对象。 + */ + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + mInstance = new GTaskManager(); + } + return mInstance; + } + + /** + * 设置活动上下文。 + * 用于获取授权令牌。 + * + * @param activity 当前活动对象。 + */ + public synchronized void setActivityContext(Activity activity) { + mActivity = activity; + } + + /** + * 同步任务数据。 + * 会尝试与Google任务进行登录和数据同步,如果过程中发生错误或取消,则返回对应的状态码。 + * + * @param context 上下文对象,用于执行同步操作。 + * @param asyncTask 异步任务对象,用于在同步过程中更新进度。 + * @return 同步操作的状态码,可以是正在同步、网络错误、内部错误或同步取消。 + */ + public int sync(Context context, GTaskASyncTask asyncTask) { + if (mSyncing) { + Log.d(TAG, "Sync is in progress"); + 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(); + + // 尝试登录 Google 任务服务 + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // 初始化 Google 任务列表 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // 执行内容同步工作 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + syncContent(); + } catch (NetworkFailureException e) { + Log.e(TAG, e.toString()); + return STATE_NETWORK_ERROR; + } catch (ActionFailureException e) { + Log.e(TAG, e.toString()); + return STATE_INTERNAL_ERROR; + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return STATE_INTERNAL_ERROR; + } finally { + // 无论成功或失败,最后都清理数据结构 + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + mSyncing = false; + } + + return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; + } + + /** + * 初始化 GTask 列表。 + * 该方法首先检查操作是否已被取消,然后从 GTaskClient 获取任务列表信息,并初始化元数据列表和任务列表。 + * 如果过程中发生网络错误,可能会抛出 NetworkFailureException 异常。 + * + * @throws NetworkFailureException 如果网络操作失败,则抛出此异常。 + */ + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) // 检查是否取消了操作 + return; + + GTaskClient client = GTaskClient.getInstance(); // 获取 GTask 客户端实例 + + try { + JSONArray jsTaskLists = client.getTaskLists(); // 从客户端获取任务列表数组 + + // 初始化元数据列表 + 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)) { + mMetaList = new TaskList(); + mMetaList.setContentByRemoteJSON(object); + + // 加载元数据 + JSONArray jsMetas = client.getTaskList(gid); + for (int j = 0; j < jsMetas.length(); j++) { + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + metaData.setContentByRemoteJSON(object); + if (metaData.isWorthSaving()) { + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // 如果元数据列表不存在,则创建新的元数据列表 + if (mMetaList == null) { + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META); + GTaskClient.getInstance().createTaskList(mMetaList); + } + + // 初始化任务列表 + 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)) { + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // 加载任务 + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + // 处理 JSON 解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handling JSONObject failed"); + } + } + + + /** + * 同步内容数据。 + * 该方法首先处理本地已删除的笔记,然后同步文件夹信息,接着处理数据库中存在的笔记, + * 最后处理剩余的项目,并更新本地同步ID。如果在过程中检测到网络失败,则抛出网络失败异常。 + * + * @throws NetworkFailureException 如果在网络通信过程中发生失败 + */ + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + + mLocalDeleteIdMap.clear(); // 清除本地删除映射表 + + if (mCancelled) { + return; // 如果操作已被取消,则直接返回 + } + + // 处理本地删除的笔记 + 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) + }, null); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + 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"); + } + } finally { + if (c != null) { + c.close(); // 关闭游标 + c = null; + } + } + + // 首先同步文件夹信息 + syncFolder(); + + // 处理数据库中存在的笔记 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[]{ + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); // 从映射表中移除 + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); // 更新ID映射 + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); // 获取同步动作 + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // 如果没有GTask ID,则视为本地新增 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // 如果有GTask ID但本地不存在,则视为远程删除 + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); // 执行内容同步 + } + } else { + Log.w(TAG, "failed to query existing note in database"); + } + + } finally { + if (c != null) { + c.close(); // 关闭游标 + c = null; + } + } + + // 处理剩余项目 + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); // 将剩余项目作为本地新增处理 + } + + // 检查是否取消操作,清理本地删除表,并更新本地同步ID + if (!mCancelled) { + // 批量删除本地已删除的笔记,如果失败则抛出异常 + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + } + + // 刷新本地同步ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); // 提交更新 + refreshLocalSyncId(); // 刷新本地同步ID + } + + } + + /** + * 同步文件夹数据。 + * 该方法负责同步根文件夹、通话记录文件夹以及本地和远程存在的文件夹。 + * 它会根据文件夹的当前状态(是否存在、是否已同步)采取相应的同步操作,如添加、更新或删除。 + * + * @throws NetworkFailureException 如果网络操作失败 + */ + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + + if (mCancelled) { + return; + } + + // 同步根文件夹 + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + if (c != null) { + c.moveToNext(); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + // 判断节点是否为空,不为空则更新,为空则添加 + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // 仅当系统文件夹的名称需要更新时执行内容同步 + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } else { + Log.w(TAG, "failed to query root folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // 同步通话记录文件夹 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[]{ + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + if (c != null) { + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + // 判断节点是否为空,不为空则更新,为空则添加 + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); + // 仅当系统文件夹的名称需要更新时执行内容同步 + if (!node.getName().equals( + 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"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // 同步本地已存在的文件夹 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[]{ + String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + // 判断节点是否为空,不为空则更新,为空则根据情况添加或删除 + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // 本地添加 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // 远程删除 + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // 同步远程添加的文件夹 + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + } + + if (!mCancelled) + 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; + } + + MetaData meta; + switch (syncType) { + case Node.SYNC_ACTION_ADD_LOCAL: // 添加本地节点 + addLocalNode(node); + break; + case Node.SYNC_ACTION_ADD_REMOTE: // 添加远程节点 + addRemoteNode(node, c); + break; + case Node.SYNC_ACTION_DEL_LOCAL: // 删除本地节点 + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); // 从服务器删除节点 + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); // 记录已删除的本地节点ID + break; + case Node.SYNC_ACTION_DEL_REMOTE: // 删除远程节点 + meta = mMetaHashMap.get(node.getGid()); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); // 从服务器删除节点 + } + GTaskClient.getInstance().deleteNode(node); // 直接从本地数据库删除节点 + break; + case Node.SYNC_ACTION_UPDATE_LOCAL: // 更新本地节点 + updateLocalNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_REMOTE: // 更新远程节点 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_CONFLICT: // 处理更新冲突 + // 目前简单地采用本地更新,未来可能需要合并双方修改 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: // 无操作 + break; + case Node.SYNC_ACTION_ERROR: // 默认错误处理 + default: + throw new ActionFailureException("unkown sync action type"); // 抛出未知同步操作类型的异常 + } + } + + + /** + * 将本地节点添加到数据库中。 + * 该方法首先检查操作是否已被取消,然后根据节点类型(任务列表或任务)创建相应的 SqlNote 对象。 + * 对于任务列表节点,会根据节点名称(默认文件夹或通话记录文件夹)设置特殊的 SqlNote 属性; + * 对于任务节点,会从节点内容中创建 JSON 对象,并根据需要调整其中的 ID 字段(如果这些 ID 在数据库中已存在)。 + * 最后,该方法会将 SqlNote 对象提交到数据库,并更新相关的 ID 映射关系。 + * + * @param node 要添加的本地节点,不应为 null。 + * @throws NetworkFailureException 如果添加节点过程中检测到网络失败。 + */ + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + return; // 如果操作已取消,则直接返回,不执行添加操作 + } + + SqlNote sqlNote; + if (node instanceof TaskList) { + // 处理任务列表节点,根据节点名称设置特殊的 SqlNote 属性 + 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 { + // 处理任务节点,从节点内容创建 JSON 对象,并根据需要调整 ID 字段 + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + // 检查并处理 JSON 对象中的 Note 和 Data ID 字段 + 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)) { + // 如果笔记 ID 已存在,则移除该 ID + 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)) { + // 如果数据 ID 已存在,则移除该 ID + data.remove(DataColumns.ID); + } + } + } + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js); + + // 设置节点的父 ID + 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()); + } + + // 提交 SqlNote 到数据库,并更新 ID 映射关系 + sqlNote.setGtaskId(node.getGid()); + sqlNote.commit(false); + + mGidToNid.put(node.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // 更新远程元数据 + updateRemoteMeta(node.getGid(), sqlNote); + } + + + /** + * 更新本地节点信息 + * + * @param node 需要更新的节点 + * @param c 数据库游标,用于操作数据库 + * @throws NetworkFailureException 如果网络操作失败,则抛出此异常 + */ + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote; + // 根据节点内容更新本地数据库中的笔记 + sqlNote = new SqlNote(mContext, c); + sqlNote.setContent(node.getLocalJSONFromContent()); + + // 确定父节点ID,任务则查找父任务ID,否则默认为根文件夹ID + 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); + + // 更新远程元数据 + updateRemoteMeta(node.getGid(), sqlNote); + } + + /** + * 在远程添加节点信息 + * + * @param node 需要添加的节点 + * @param c 数据库游标,用于操作数据库 + * @throws NetworkFailureException 如果网络操作失败,则抛出此异常 + */ + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + Node n; + + // 如果是任务类型,则远程创建任务;否则,根据条件判断是否需要创建新的任务列表 + if (sqlNote.isNoteType()) { + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + // 查找任务所属的任务列表ID + 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; + + // 更新远程元数据 + updateRemoteMeta(task.getGid(), sqlNote); + } else { + TaskList tasklist = null; + + // 判断是否需要创建新的任务列表 + 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; + } + } + + // 如果没有找到匹配的任务列表,则创建新的任务列表 + if (tasklist == null) { + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist; + } + + // 更新本地数据库中的笔记信息 + sqlNote.setGtaskId(n.getGid()); + sqlNote.commit(false); + sqlNote.resetLocalModified(); + sqlNote.commit(true); + + // 更新GID和ID的映射关系 + mGidToNid.put(n.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), n.getGid()); + } + + + /** + * 更新远程节点信息。 + * + * @param node 需要更新的节点 + * @param c 数据库游标,用于获取节点的详细信息 + * @throws NetworkFailureException 如果网络操作失败,则抛出此异常 + */ + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { // 检查是否已取消操作 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); // 从数据库游标中创建 SqlNote 对象 + + // 使用本地 JSON 格式更新远程节点内容 + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); // 将节点添加到更新队列 + + // 更新元数据 + updateRemoteMeta(node.getGid(), sqlNote); + + // 如果是笔记类型,检查并移动任务 + if (sqlNote.isNoteType()) { + Task task = (Task) node; + TaskList preParentList = task.getParent(); // 获取任务的当前父任务列表 + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); // 获取当前父任务列表的 GID + if (curParentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot update remote task"); + } + TaskList curParentList = mGTaskListHashMap.get(curParentGid); // 获取当前父任务列表对象 + + // 如果任务的父任务列表发生变化,则移动任务 + if (preParentList != curParentList) { + preParentList.removeChildTask(task); + curParentList.addChildTask(task); + GTaskClient.getInstance().moveTask(task, preParentList, curParentList); + } + } + + // 重置本地修改标志,并提交更改 + sqlNote.resetLocalModified(); + sqlNote.commit(true); + } + + /** + * 更新远程元数据。 + * + * @param gid 元数据的全局标识符 + * @param sqlNote 包含元数据内容的 SqlNote 对象 + * @throws NetworkFailureException 如果网络操作失败,则抛出此异常 + */ + 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); + } + } + } + + + /** + * 刷新本地同步ID。 + * 该方法首先获取最新的gtask列表,然后通过查询本地笔记内容来更新这些笔记的同步ID。 + * 如果在查询过程中发现有本地项目在同步后没有对应的gtask ID,则抛出ActionFailureException异常。 + * + * @throws NetworkFailureException 如果网络操作失败。 + * @throws ActionFailureException 如果在同步后发现有本地项目没有对应的gtask ID。 + */ + private void refreshLocalSyncId() throws NetworkFailureException { + if (mCancelled) { + return; + } + + // 清空现有的gtask列表和元数据,准备获取最新的数据 + mGTaskHashMap.clear(); + mGTaskListHashMap.clear(); + mMetaHashMap.clear(); + initGTaskList(); + + Cursor c = null; + try { + // 查询本地笔记内容,准备更新同步ID + 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 { + // 如果查询到的笔记没有对应的gtask ID,则抛出异常 + 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 { + // 释放Cursor资源 + if (c != null) { + c.close(); + c = null; + } + } + } + + /** + * 获取同步账户的名称。 + * + * @return 同步账户的名称。 + */ + public String getSyncAccount() { + return GTaskClient.getInstance().getSyncAccount().name; + } + + /** + * 取消同步操作。 + * 设置取消标志,终止正在进行的同步操作。 + */ + public void cancelSync() { + mCancelled = true; + } + +} diff --git a/src/gtask/remote/GTaskSyncService.java b/src/gtask/remote/GTaskSyncService.java new file mode 100644 index 0000000..388b08e --- /dev/null +++ b/src/gtask/remote/GTaskSyncService.java @@ -0,0 +1,169 @@ +/* + * GTaskSyncService类用于处理与Google任务同步相关的服务操作。 + */ +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +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(""); // 发送空消息,表示同步完成 + stopSelf(); // 停止服务 + } + }); + sendBroadcast(""); // 发送初始同步状态 + mSyncTask.execute(); // 执行同步任务 + } + } + + /* + * 取消当前的同步任务。 + */ + private void cancelSync() { + // 如果有正在执行的同步任务,调用取消方法 + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + /* + * 服务创建时的初始化操作。 + * 重置同步任务为null,准备服务的生命周期。 + */ + @Override + public void onCreate() { + mSyncTask = null; // 确保没有正在执行的任务 + } + + /* + * 处理服务启动时的命令。 + * 根据传入的意图参数决定是启动同步还是取消同步。 + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Bundle bundle = intent.getExtras(); // 获取传入的附加数据 + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据操作类型决定行为 + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: + startSync(); // 启动同步 + break; + case ACTION_CANCEL_SYNC: + cancelSync(); // 取消同步 + break; + default: + break; // 无效操作,忽略 + } + return START_STICKY; // 服务将在系统资源不足时重启 + } + return super.onStartCommand(intent, flags, startId); // 默认处理 + } + + /* + * 低内存时调用,取消同步任务。 + */ + @Override + public void onLowMemory() { + // 如果有正在执行的同步任务,则取消 + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + // 服务绑定时返回null,此服务不提供绑定功能 + public IBinder onBind(Intent intent) { + return null; // 不支持绑定,返回null + } + + /* + * 发送同步状态的广播。 + * 更新同步进度,并通过广播发送当前的同步状态和进度消息。 + */ + public void sendBroadcast(String msg) { + mSyncProgress = msg; // 更新进度消息 + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); // 创建广播意图 + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); // 添加同步状态 + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); // 添加进度消息 + sendBroadcast(intent); // 发送广播 + } + + /* + * 从Activity启动同步。 + * 设置活动上下文并启动同步服务。 + */ + public static void startSync(Activity activity) { + GTaskManager.getInstance().setActivityContext(activity); // 记录活动上下文 + Intent intent = new Intent(activity, GTaskSyncService.class); // 创建意图 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); // 添加入参 + activity.startService(intent); // 启动服务 + } + + /* + * 从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); // 启动服务以取消同步 + } + + /* + * 检查是否正在同步。 + * 返回当前是否有一个同步任务在执行。 + */ + public static boolean isSyncing() { + return mSyncTask != null; // 返回同步任务是否在进行 + } + + /* + * 获取同步进度的字符串。 + * 返回当前同步任务的进度消息。 + */ + public static String getProgressString() { + return mSyncProgress; // 返回进度消息 + } +} diff --git a/src/model/Note.java b/src/model/Note.java new file mode 100644 index 0000000..a926d63 --- /dev/null +++ b/src/model/Note.java @@ -0,0 +1,254 @@ +// 包声明和导入必要的Android框架包以及项目特定的数据类 + +public class Note { + // 私有成员变量,用来保存笔记的修改值和数据内容 + private ContentValues mNoteDiffValues; + private NoteData mNoteData; + + // 日志标签,方便在日志中识别输出信息来源于哪个类 + private static final String TAG = "Note"; + + /** + * 获取一个新的笔记ID,用于添加新的笔记到数据库。 + * 该方法是静态同步的,确保同一时间只有一个线程可以获取新ID,以避免冲突。 + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // 创建一个ContentValues实例,用于存储要插入数据库的笔记信息 + ContentValues values = new ContentValues(); + // 设置创建时间和修改时间为当前时间戳 + long createdTime = System.currentTimeMillis(); + values.put(NoteColumns.CREATED_DATE, createdTime); + values.put(NoteColumns.MODIFIED_DATE, createdTime); + // 设置笔记类型为普通笔记 + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + // 标记为本地修改 + values.put(NoteColumns.LOCAL_MODIFIED, 1); + // 设置父级文件夹ID + values.put(NoteColumns.PARENT_ID, folderId); + + // 插入笔记记录并获取返回的URI + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + + // 解析URI路径片段以获得新插入笔记的ID + long noteId = 0; + try { + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId; // 返回新创建的笔记ID + } + + // 构造函数初始化成员变量 + public Note() { + mNoteDiffValues = new ContentValues(); + mNoteData = new NoteData(); + } + + // 设置笔记属性值的方法 + public void setNoteValue(String key, String value) { + // 更新或设置指定键对应的值,并标记为本地修改 + mNoteDiffValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + // 设置文本数据的方法 + public void setTextData(String key, String value) { + mNoteData.setTextData(key, value); + } + + // 设置文本数据ID的方法 + public void setTextDataId(long id) { + mNoteData.setTextDataId(id); + } + + // 获取文本数据ID的方法 + public long getTextDataId() { + return mNoteData.mTextDataId; + } + + // 设置通话记录数据ID的方法 + public void setCallDataId(long id) { + mNoteData.setCallDataId(id); + } + + // 设置通话记录数据的方法 + public void setCallData(String key, String value) { + mNoteData.setCallData(key, value); + } + + // 检查是否有本地修改过的数据 + public boolean isLocalModified() { + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } + + /** + * 同步笔记到数据库的方法。 + * 如果笔记ID无效则抛出异常;如果没有本地修改,则直接返回true。 + * 尝试更新笔记记录,即使失败也会继续尝试更新数据部分。 + */ + public boolean syncNote(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + if (!isLocalModified()) { + return true; + } + + // 更新笔记的基本信息 + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + Log.e(TAG, "Update note error, should not happen"); + // 即使更新失败也继续执行后续操作 + } + mNoteDiffValues.clear(); // 清除已经同步的内容 + + // 同步额外的数据(如文本或通话记录) + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; + } + + return true; + } + + // 内部类NoteData,用来处理与笔记关联的详细数据 + private class NoteData { + // 成员变量,包括文本数据ID、通话记录数据ID及其对应的值集合 + private long mTextDataId; + private ContentValues mTextDataValues; + private long mCallDataId; + private ContentValues mCallDataValues; + + // 日志标签 + private static final String TAG = "NoteData"; + + // 构造函数初始化成员变量 + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } + + // 检查是否有本地修改过的数据 + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } + + // 设置文本数据ID的方法 + void setTextDataId(long id) { + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); + } + mTextDataId = id; + } + + // 设置通话记录数据ID的方法 + void setCallDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); + } + mCallDataId = id; + } + + // 设置通话记录数据的方法 + void setCallData(String key, String value) { + mCallDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + // 设置文本数据的方法 + void setTextData(String key, String value) { + mTextDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 将数据推送到ContentResolver,进行数据库更新或插入。 + * 对于文本数据和通话记录数据,分别根据是否已有ID来决定是更新还是插入新记录。 + */ + Uri pushIntoContentResolver(Context context, long noteId) { + // 参数校验 + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + ArrayList operationList = new ArrayList<>(); + ContentProviderOperation.Builder builder = null; + + // 处理文本数据 + if(mTextDataValues.size() > 0) { + mTextDataValues.put(DataColumns.NOTE_ID, noteId); + if (mTextDataId == 0) { + // 插入新的文本数据记录 + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mTextDataValues); + try { + setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new text data fail with noteId" + noteId); + mTextDataValues.clear(); + return null; + } + } else { + // 更新现有的文本数据记录 + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear(); + } + + // 处理通话记录数据 + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); + if (mCallDataId == 0) { + // 插入新的通话记录数据记录 + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mCallDataValues); + try { + setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new call data fail with noteId" + noteId); + mCallDataValues.clear(); + return null; + } + } else { + // 更新现有的通话记录数据记录 + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear(); + } + + // 执行批量操作 + if (operationList.size() > 0) { + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, operationList); + return (results == null || results.length == 0 || results[0] == null) ? null + : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/model/WorkingNote.java b/src/model/WorkingNote.java new file mode 100644 index 0000000..98a2d81 --- /dev/null +++ b/src/model/WorkingNote.java @@ -0,0 +1,289 @@ +// 导入必要的包和类,这些类用于处理数据存储、内容提供者操作等。 +import net.micode.notes.data.Notes.CallNote; +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.Notes.TextNote; +import net.micode.notes.tool.ResourceParser.NoteBgResources; + +// 定义WorkingNote类,该类负责处理工作笔记的具体逻辑。 +public class WorkingNote { + // 成员变量声明,用于存储笔记的相关信息,例如:笔记对象、ID、内容、模式、提醒日期等。 + private Note mNote; // 笔记对象 + private long mNoteId; // 笔记ID + private String mContent; // 笔记内容 + private int mMode; // 笔记模式(如普通笔记或清单模式) + private long mAlertDate; // 提醒时间 + private long mModifiedDate; // 修改时间 + private int mBgColorId; // 背景颜色ID + private int mWidgetId; // 小部件ID + private int mWidgetType; // 小部件类型 + private long mFolderId; // 文件夹ID + private Context mContext; // 上下文环境 + private static final String TAG = "WorkingNote"; // 日志标签 + private boolean mIsDeleted; // 标记笔记是否被删除 + private NoteSettingChangedListener mNoteSettingStatusListener; // 笔记设置状态变化监听器 + + // 定义列名投影数组,用于查询时指定需要获取的字段。 + public static final String[] DATA_PROJECTION = new String[]{ + DataColumns.ID, DataColumns.CONTENT, DataColumns.MIME_TYPE, + DataColumns.DATA1, DataColumns.DATA2, DataColumns.DATA3, DataColumns.DATA4 + }; + + public static final String[] NOTE_PROJECTION = new String[]{ + NoteColumns.PARENT_ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, + NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.MODIFIED_DATE + }; + + // 定义列索引常量,方便访问查询结果中的特定列。 + private static final int DATA_ID_COLUMN = 0; + private static final int DATA_CONTENT_COLUMN = 1; + private static final int DATA_MIME_TYPE_COLUMN = 2; + private static final int DATA_MODE_COLUMN = 3; + private static final int NOTE_PARENT_ID_COLUMN = 0; + private static final int NOTE_ALERTED_DATE_COLUMN = 1; + private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + private static final int NOTE_WIDGET_ID_COLUMN = 3; + private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + + // 构造函数,用于创建新的工作笔记实例。 + private WorkingNote(Context context, long folderId) { + mContext = context; + mAlertDate = 0; + mModifiedDate = System.currentTimeMillis(); // 设置当前时间为修改时间 + mFolderId = folderId; + mNote = new Note(); + mNoteId = 0; + mIsDeleted = false; + mMode = 0; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 初始化为无效的小部件类型 + } + + // 构造函数,用于加载已有的工作笔记实例。 + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context; + mNoteId = noteId; + mFolderId = folderId; + mIsDeleted = false; + mNote = new Note(); + loadNote(); // 加载笔记数据 + } + + // 加载笔记的方法,从数据库中读取笔记的数据。 + private void loadNote() { + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); + + if (cursor != null && cursor.moveToFirst()) { + do { + mNote.setNoteValue(NoteColumns.PARENT_ID, cursor.getString(NOTE_PARENT_ID_COLUMN)); + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(mBgColorId)); + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + mNote.setNoteValue(NoteColumns.MODIFIED_DATE, String.valueOf(mModifiedDate)); + } while (cursor.moveToNext()); + cursor.close(); + } + } + + // 其他方法... + + // 创建空笔记的静态方法。 + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId); + note.setBgColorId(defaultBgColorId); // 设置默认背景色 + note.setWidgetId(widgetId); // 设置小部件ID + note.setWidgetType(widgetType); // 设置小部件类型 + return note; + } + + // 从数据库加载已有笔记的静态方法。 + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); + } + + // 保存笔记的方法,检查笔记是否有值得保存的内容,并更新数据库。 + public synchronized boolean saveNote() { + if (isWorthSaving()) { // 检查笔记是否有值得保存的内容 + if (!existInDatabase()) { // 如果笔记不在数据库中,则尝试创建新笔记 + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + Log.e(TAG, "Create new note fail with id:" + mNoteId); + return false; + } + } + mNote.syncNote(mContext, mNoteId); // 同步笔记到数据库 + + // 更新关联的小部件内容(如果存在) + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } + + // 判断笔记是否存在数据库中的方法。 + public boolean existInDatabase() { + return mNoteId > 0; + } + + // 判断笔记是否有值得保存的内容。 + private boolean isWorthSaving() { + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())) { + return false; + } else { + return true; + } + } + + // 设置背景颜色ID的方法。 + public void setBgColorId(int id) { + if (id != mBgColorId) { // 只有当新旧背景色不同才更新 + mBgColorId = id; + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知监听器背景色改变 + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 更新笔记属性 + } + } + + // 设置清单模式的方法。 + public void setCheckListMode(int mode) { + if (mMode != mode) { // 只有当新旧模式不同时才更新 + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); // 通知监听器模式改变 + } + mMode = mode; + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 更新笔记文本数据 + } + } + + // 设置小部件类型的内部方法。 + public void setWidgetType(int type) { + if (type != mWidgetType) { // 只有当新旧类型不同时才更新 + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 更新笔记属性 + } + } + + // 设置小部件ID的内部方法。 + public void setWidgetId(int id) { + if (id != mWidgetId) { // 只有当新旧ID不同时才更新 + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 更新笔记属性 + } + } + + // 设置工作文本内容的方法。 + public void setWorkingText(String text) { + if (!TextUtils.equals(mContent, text)) { // 只有当文本内容发生变化时才更新 + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent); // 更新笔记文本数据 + } + } + + // 将普通笔记转换为通话记录笔记的方法。 + public void convertToCallNote(String phoneNumber, long callDate) { + mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); // 设置通话日期 + mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); // 设置电话号码 + mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); // 移动到通话记录文件夹 + } + + // 判断是否有设置提醒的方法。 + public boolean hasClockAlert() { + return (mAlertDate > 0 ? true : false); + } + + // 获取内容的方法。 + public String getContent() { + return mContent; + } + + // 获取提醒日期的方法。 + public long getAlertDate() { + return mAlertDate; + } + + // 获取修改日期的方法。 + public long getModifiedDate() { + return mModifiedDate; + } + + // 获取背景颜色资源ID的方法。 + public int getBgColorResId() { + return NoteBgResources.getNoteBgResource(mBgColorId); + } + + // 获取背景颜色ID的方法。 + public int getBgColorId() { + return mBgColorId; + } + + // 获取标题背景颜色资源ID的方法。 + public int getTitleBgResId() { + return NoteBgResources.getNoteTitleBgResource(mBgColorId); + } + + // 获取清单模式的方法。 + public int getCheckListMode() { + return mMode; + } + + // 获取笔记ID的方法。 + public long getNoteId() { + return mNoteId; + } + + // 获取文件夹ID的方法。 + public long getFolderId() { + return mFolderId; + } + + // 获取小部件ID的方法。 + public int getWidgetId() { + return mWidgetId; + } + + // 获取小部件类型的方法。 + public int getWidgetType() { + return mWidgetType; + } + + // 设置提醒日期的方法。 + public void setAlertDate(long date, boolean set) { + if (date != mAlertDate) { + mAlertDate = date; + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); // 更新笔记属性 + } + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onClockAlertChanged(date, set); // 通知监听器提醒改变 + } + } + + // 标记笔记为删除状态的方法。 + public void markDeleted(boolean mark) { + mIsDeleted = mark; + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); // 通知监听器小部件改变 + } + } + + // 设置设置状态变化监听器的方法。 + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; + } +} \ No newline at end of file diff --git a/src/tool/BackupUtils.java b/src/tool/BackupUtils.java new file mode 100644 index 0000000..821cbd6 --- /dev/null +++ b/src/tool/BackupUtils.java @@ -0,0 +1,279 @@ + +/* + * BackupUtils 工具类 + * 提供了笔记备份到文本文件的功能。 + */ + +package net.micode.notes.tool; + +// 引入需要用到的 Android 和 Java 标准库类 +import android.content.Context; +import android.database.Cursor; +import android.os.Environment; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + +/** + * BackupUtils 是一个工具类,用于实现笔记的备份功能。 + */ +public class BackupUtils { + + // 日志标志,用于记录类中的日志 + private static final String TAG = "BackupUtils"; + + // 单例模式:存储唯一实例 + private static BackupUtils sInstance; + + /** + * 获取 BackupUtils 类的单例实例。 + * + * @param context 上下文对象,用于初始化内部组件。 + * @return 返回 BackupUtils 实例。 + */ + public static synchronized BackupUtils getInstance(Context context) { + if (sInstance == null) { // 如果实例不存在,则创建新实例 + sInstance = new BackupUtils(context); + } + return sInstance; // 返回单例实例 + } + + /** + * 定义备份操作的状态常量 + */ + public static final int STATE_SD_CARD_UNMOUONTED = 0; // SD卡未挂载 + public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // 备份文件不存在 + public static final int STATE_DATA_DESTROIED = 2; // 数据已损坏 + public static final int STATE_SYSTEM_ERROR = 3; // 系统错误 + public static final int STATE_SUCCESS = 4; // 备份成功 + + // 用于文本导出的工具类 + private TextExport mTextExport; + + /** + * 私有化构造函数,防止直接实例化。 + * + * @param context 上下文对象,用于初始化导出工具类。 + */ + private BackupUtils(Context context) { + mTextExport = new TextExport(context); // 初始化文本导出工具 + } + + /** + * 检查 SD 卡是否可用。 + * + * @return 如果 SD 卡已挂载,则返回 true,否则返回 false。 + */ + private static boolean externalStorageAvailable() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + /** + * 导出笔记到文本文件。 + * + * @return 返回导出操作的状态码。 + */ + public int exportToText() { + return mTextExport.exportToText(); // 调用 TextExport 类中的导出方法 + } + + /** + * 获取导出文本的文件名。 + * + * @return 返回文件名字符串。 + */ + public String getExportedTextFileName() { + return mTextExport.mFileName; // 返回文件名 + } + + /** + * 获取导出文件的目录。 + * + * @return 返回目录路径字符串。 + */ + public String getExportedTextFileDir() { + return mTextExport.mFileDirectory; // 返回文件目录路径 + } + + /** + * 内部类,用于执行文本导出功能。 + */ + private static class TextExport { + + // 定义查询笔记的列名 + private static final String[] NOTE_PROJECTION = { + NoteColumns.ID, // 笔记 ID + NoteColumns.MODIFIED_DATE, // 修改时间 + NoteColumns.SNIPPET, // 摘要 + NoteColumns.TYPE // 类型 + }; + + // 定义笔记列的索引 + private static final int NOTE_COLUMN_ID = 0; // ID 列索引 + private static final int NOTE_COLUMN_MODIFIED_DATE = 1; // 修改时间列索引 + private static final int NOTE_COLUMN_SNIPPET = 2; // 摘要列索引 + + // 定义查询数据表的列 + private static final String[] DATA_PROJECTION = { + DataColumns.CONTENT, // 内容 + DataColumns.MIME_TYPE, // MIME 类型 + DataColumns.DATA1, // 数据 1 + DataColumns.DATA2, // 数据 2 + DataColumns.DATA3, // 数据 3 + DataColumns.DATA4, // 数据 4 + }; + + // 定义数据列的索引 + private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引 + private static final int DATA_COLUMN_MIME_TYPE = 1; // MIME 类型列索引 + + // 上下文对象,用于访问系统资源 + private Context mContext; + private String mFileName; // 文件名 + private String mFileDirectory; // 文件目录 + + // 初始化文本导出格式 + private final String[] TEXT_FORMAT; + + public TextExport(Context context) { + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); // 导出格式 + mContext = context; + mFileName = ""; + mFileDirectory = ""; + } + + /** + * 根据文件夹 ID 将笔记导出为文本。 + * + * @param folderId 文件夹 ID。 + * @param ps 打印流,用于写入文本内容。 + */ + private void exportFolderToText(String folderId, PrintStream ps) { + Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, + NoteColumns.PARENT_ID + "=?", new String[]{folderId}, null); + + if (notesCursor != null) { + if (notesCursor.moveToFirst()) { + do { + // 输出笔记的修改时间 + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // 导出笔记内容 + String noteId = notesCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (notesCursor.moveToNext()); + } + notesCursor.close(); + } + } + + /** + * 导出指定笔记 ID 的内容到文本。 + * + * @param noteId 笔记 ID。 + * @param ps 打印流。 + */ + private void exportNoteToText(String noteId, PrintStream ps) { + Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[]{noteId}, null); + + if (dataCursor != null) { + if (dataCursor.moveToFirst()) { + do { + String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + if (DataConstants.CALL_NOTE.equals(mimeType)) { + // 导出通话笔记 + } else if (DataConstants.NOTE.equals(mimeType)) { + // 导出普通笔记 + } + } while (dataCursor.moveToNext()); + } + dataCursor.close(); + } + } + + /** + * 获取指向文件的打印流,用于导出笔记内容。 + * + * @return 返回指向生成的文本文件的打印流对象,如果失败则返回 null。 + */ + private PrintStream getExportToTextPrintStream() { + // 调用辅助方法,生成导出文件 + File file = generateFileMountedOnSDcard(mContext, R.string.file_path, R.string.file_name_txt_format); + if (file == null) { // 如果文件生成失败 + Log.e(TAG, "create file to exported failed"); // 输出错误日志 + return null; // 返回 null,表示打印流创建失败 + } + // 更新文件名和目录信息 + mFileName = file.getName(); // 设置文件名 + mFileDirectory = mContext.getString(R.string.file_path); // 设置文件目录 + + PrintStream ps = null; // 初始化打印流为 null + try { + // 创建文件输出流并包装为打印流 + FileOutputStream fos = new FileOutputStream(file); + ps = new PrintStream(fos); + } catch (FileNotFoundException e) { // 捕获文件未找到异常 + e.printStackTrace(); + return null; // 返回 null,表示打印流创建失败 + } catch (NullPointerException e) { // 捕获空指针异常 + e.printStackTrace(); + return null; // 返回 null + } + return ps; // 返回成功创建的打印流对象 + } + + /** + * 在 SD 卡上生成用于存储导出数据的文件。 + * + * @param context 上下文对象,用于访问应用资源。 + * @param filePathResId 文件路径的资源 ID。 + * @param fileNameFormatResId 文件名格式的资源 ID,用于生成包含日期的文件名。 + * @return 返回生成的文件对象,如果失败则返回 null。 + */ + private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { + StringBuilder sb = new StringBuilder(); // 使用 StringBuilder 拼接路径和文件名 + + // 获取 SD 卡的根目录并拼接路径 + sb.append(Environment.getExternalStorageDirectory()); + sb.append(context.getString(filePathResId)); // 从资源中获取路径字符串 + + // 创建文件目录对象 + File filedir = new File(sb.toString()); + // 生成文件名并拼接到路径中,文件名包含日期信息 + sb.append(context.getString(fileNameFormatResId, + DateFormat.format(context.getString(R.string.format_date_ymd), System.currentTimeMillis()))); + File file = new File(sb.toString()); // 创建文件对象 + + try { + if (!filedir.exists()) { // 如果目录不存在 + filedir.mkdir(); // 创建目录 + } + if (!file.exists()) { // 如果文件不存在 + file.createNewFile(); // 创建文件 + } + return file; // 返回成功生成的文件对象 + } catch (SecurityException e) { // 捕获安全异常 + e.printStackTrace(); + } catch (IOException e) { // 捕获 I/O 异常 + e.printStackTrace(); + } + return null; // 如果发生异常,返回 null + } + } +} + + diff --git a/src/tool/DataUtils.java b/src/tool/DataUtils.java new file mode 100644 index 0000000..977ad28 --- /dev/null +++ b/src/tool/DataUtils.java @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * Licensed under the Apache License, Version 2.0 + * 该文件定义了用于管理和操作笔记数据的实用工具类 DataUtils。 + */ + +package net.micode.notes.tool; // 声明该类所属的包路径 + +// 导入需要的类,用于操作内容提供器、数据库、日志和其他功能 +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; // 导入笔记数据定义 +import net.micode.notes.data.Notes.CallNote; // 导入与通话笔记相关的定义 +import net.micode.notes.data.Notes.NoteColumns; // 导入与笔记列相关的定义 +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; // 导入笔记小部件属性定义 + +import java.util.ArrayList; // 用于创建动态数组 +import java.util.HashSet; // 用于存储无重复的集合 + +// 定义 DataUtils 工具类 +public class DataUtils { + public static final String TAG = "DataUtils"; // 定义日志标记常量 + + /** + * 批量删除笔记 + * + * @param resolver 内容解析器 + * @param ids 要删除的笔记ID集合 + * @return 如果删除成功或集合为空或为null,则返回true,否则返回false + */ + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + if (ids == null) { // 如果传入的ID集合为null + Log.d(TAG, "the ids is null"); // 输出调试日志 + return true; // 返回true,表示没有需要删除的内容 + } + if (ids.size() == 0) { // 如果ID集合为空 + Log.d(TAG, "no id is in the hashset"); // 输出调试日志 + return true; // 返回true + } + + // 创建一个操作列表,用于批量删除操作 + ArrayList operationList = new ArrayList(); + for (long id : ids) { // 遍历需要删除的ID集合 + if (id == Notes.ID_ROOT_FOLDER) { // 如果ID是系统根文件夹 + Log.e(TAG, "Don't delete system folder root"); // 输出错误日志 + continue; // 跳过此ID + } + // 创建删除操作 + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + operationList.add(builder.build()); // 将删除操作添加到列表 + } + try { + // 批量执行删除操作 + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { // 检查删除结果 + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); // 输出调试日志 + return false; // 删除失败,返回false + } + return true; // 删除成功,返回true + } catch (RemoteException e) { // 捕获远程异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 + } catch (OperationApplicationException e) { // 捕获操作应用异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 + } + return false; // 出现异常时,返回false + } + + /** + * 将笔记移动到指定文件夹 + * + * @param resolver 内容解析器 + * @param id 笔记ID + * @param srcFolderId 原始文件夹ID + * @param desFolderId 目标文件夹ID + */ + public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { + ContentValues values = new ContentValues(); // 创建一个ContentValues对象 + values.put(NoteColumns.PARENT_ID, desFolderId); // 设置目标文件夹ID + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 设置原始文件夹ID + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改 + // 更新指定笔记的文件夹信息 + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); + } + + /** + * 批量将笔记移动到指定文件夹 + * + * @param resolver 内容解析器 + * @param ids 要移动的笔记ID集合 + * @param folderId 目标文件夹ID + * @return 如果移动成功或集合为空或为null,则返回true,否则返回false + */ + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { + if (ids == null) { // 如果ID集合为null + Log.d(TAG, "the ids is null"); // 输出调试日志 + return true; // 返回true + } + + // 构建更新操作的列表 + ArrayList operationList = new ArrayList(); + for (long id : ids) { // 遍历ID集合 + // 创建更新操作 + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置目标文件夹ID + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改 + operationList.add(builder.build()); // 将操作添加到列表 + } + + try { + // 批量执行更新操作 + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { // 检查操作结果 + Log.d(TAG, "move notes failed, ids:" + ids.toString()); // 输出调试日志 + return false; // 操作失败 + } + return true; // 操作成功 + } catch (RemoteException e) { // 捕获远程异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 + } catch (OperationApplicationException e) { // 捕获操作应用异常 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 + } + return false; // 出现异常时,返回false + } + + /** + * 获取除系统文件夹外的所有用户文件夹数量 + * + * @param resolver 内容解析器 + * @return 用户文件夹数量 + */ + public static int getUserFolderCount(ContentResolver resolver) { + // 查询数据库获取用户文件夹数量,排除系统垃圾箱文件夹 + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String[]{"COUNT(*)"}, // 查询列:计数 + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", // 条件:文件夹类型且不是垃圾箱 + new String[]{String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, // 条件值 + null); // 排序:无 + + int count = 0; // 初始化文件夹计数 + if (cursor != null) { // 如果查询结果不为空 + if (cursor.moveToFirst()) { // 移动到第一条记录 + try { + count = cursor.getInt(0); // 获取计数值 + } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 + Log.e(TAG, "get folder count failed:" + e.toString()); // 输出错误日志 + } finally { + cursor.close(); // 关闭游标 + } + } + } + return count; // 返回文件夹数量 + } +} + /** + * 检查指定类型的笔记在数据库中是否可见 + * + * @param resolver 内容解析器 + * @param noteId 笔记ID + * @param type 笔记类型 + * @return 如果可见,则返回true,否则返回false + */ + public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + // 查询数据库,检查指定类型的笔记是否存在且不在垃圾箱中 + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 查询的URI + null, // 查询的列,null表示查询所有列 + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, // 条件 + new String[]{String.valueOf(type)}, // 条件值 + null); // 排序:无 + + boolean exist = false; // 初始化结果为false + if (cursor != null) { // 如果查询结果不为空 + if (cursor.getCount() > 0) { // 如果查询结果有记录 + exist = true; // 设置结果为true + } + cursor.close(); // 关闭游标 + } + return exist; // 返回结果 + } + + /** + * 检查指定的笔记ID在数据库中是否存在 + * + * @param resolver 内容解析器 + * @param noteId 笔记ID + * @return 如果存在,则返回true,否则返回false + */ + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + // 查询数据库,检查笔记是否存在 + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 查询的URI + null, // 查询所有列 + null, // 无条件 + null, // 无条件值 + null); // 无排序 + + boolean exist = false; // 初始化结果为false + if (cursor != null) { // 如果查询结果不为空 + if (cursor.getCount() > 0) { // 如果查询结果有记录 + exist = true; // 设置结果为true + } + cursor.close(); // 关闭游标 + } + return exist; // 返回结果 + } + + /** + * 检查指定的数据ID在数据库中是否存在 + * + * @param resolver 内容解析器 + * @param dataId 数据ID + * @return 如果存在,则返回true,否则返回false + */ + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + // 查询数据库,检查数据是否存在 + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), // 查询的URI + null, // 查询所有列 + null, // 无条件 + null, // 无条件值 + null); // 无排序 + + boolean exist = false; // 初始化结果为false + if (cursor != null) { // 如果查询结果不为空 + if (cursor.getCount() > 0) { // 如果查询结果有记录 + exist = true; // 设置结果为true + } + cursor.close(); // 关闭游标 + } + return exist; // 返回结果 + } + + /** + * 检查文件夹名称是否在数据库中已存在(不包括系统文件夹) + * + * @param resolver 内容解析器 + * @param name 文件夹名称 + * @return 如果已存在,则返回true,否则返回false + */ + public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { + // 查询数据库,检查文件夹名称是否存在且不在垃圾箱中 + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, // 查询的URI + null, // 查询所有列 + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + // 条件:类型为文件夹 + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + // 且不在垃圾箱 + " AND " + NoteColumns.SNIPPET + "=?", // 且名称匹配 + new String[]{name}, // 条件值 + null); // 无排序 + + boolean exist = false; // 初始化结果为false + if (cursor != null) { // 如果查询结果不为空 + if (cursor.getCount() > 0) { // 如果查询结果有记录 + exist = true; // 设置结果为true + } + cursor.close(); // 关闭游标 + } + return exist; // 返回结果 + } + + /** + * 获取指定文件夹中的笔记小部件信息集合 + * + * @param resolver 内容解析器 + * @param folderId 文件夹ID + * @return 笔记小部件信息集合 + */ + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + // 查询数据库,获取文件夹中的小部件信息 + Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, // 查询的URI + new String[]{NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE}, // 查询的列 + NoteColumns.PARENT_ID + "=?", // 条件:父ID为指定文件夹 + new String[]{String.valueOf(folderId)}, // 条件值 + null); // 无排序 + + HashSet set = null; // 初始化小部件集合为null + if (c != null) { // 如果查询结果不为空 + if (c.moveToFirst()) { // 移动到第一条记录 + set = new HashSet(); // 初始化小部件集合 + do { // 遍历查询结果 + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); // 创建小部件属性对象 + widget.widgetId = c.getInt(0); // 获取小部件ID + widget.widgetType = c.getInt(1); // 获取小部件类型 + set.add(widget); // 将小部件添加到集合 + } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 + Log.e(TAG, e.toString()); // 输出错误日志 + } + } while (c.moveToNext()); // 移动到下一条记录 + } + c.close(); // 关闭游标 + } + return set; // 返回小部件集合 + } + + + /** + * 通过笔记ID获取关联的通话号码 + * + * @param resolver 内容解析器 + * @param noteId 笔记ID + * @return 通话号码,如果未找到则返回空字符串 + */ + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + // 查询数据库,获取与笔记ID关联的通话号码 + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, // 查询的URI + new String[]{CallNote.PHONE_NUMBER}, // 查询的列:电话号码 + CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", // 条件:匹配笔记ID和MIME类型 + new String[]{String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE}, // 条件值 + null); // 无排序 + + if (cursor != null && cursor.moveToFirst()) { // 如果查询结果不为空且有记录 + try { + return cursor.getString(0); // 返回查询结果中的电话号码 + } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 + Log.e(TAG, "Get call number fails " + e.toString()); // 输出错误日志 + } finally { + cursor.close(); // 关闭游标 + } + } + return ""; // 未找到记录时返回空字符串 + } + + /** + * 根据电话号码和通话日期获取对应的笔记ID + * + * @param resolver 内容解析器 + * @param phoneNumber 电话号码 + * @param callDate 通话日期 + * @return 笔记ID,未找到则返回0 + */ + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + // 查询数据库,根据电话号码和通话日期获取笔记ID + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, // 查询的URI + new String[]{CallNote.NOTE_ID}, // 查询的列:笔记ID + CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" + + CallNote.PHONE_NUMBER + ",?)", // 条件:匹配通话日期、MIME类型和电话号码 + new String[]{String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber}, // 条件值 + null); // 无排序 + + if (cursor != null) { // 如果查询结果不为空 + if (cursor.moveToFirst()) { // 如果有记录 + try { + return cursor.getLong(0); // 返回笔记ID + } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 + Log.e(TAG, "Get call note id fails " + e.toString()); // 输出错误日志 + } + } + cursor.close(); // 关闭游标 + } + return 0; // 未找到记录时返回0 + } + + /** + * 根据笔记ID从数据库中获取笔记的摘要 + * + * @param resolver 内容解析器 + * @param noteId 笔记的ID + * @return 笔记的摘要字符串。如果找不到对应的笔记,将抛出IllegalArgumentException。 + */ + public static String getSnippetById(ContentResolver resolver, long noteId) { + // 查询数据库,获取指定ID笔记的摘要 + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, // 查询的URI + new String[]{NoteColumns.SNIPPET}, // 查询的列:摘要 + NoteColumns.ID + "=?", // 条件:匹配笔记ID + new String[]{String.valueOf(noteId)}, // 条件值 + null); // 无排序 + + if (cursor != null) { // 如果查询结果不为空 + String snippet = ""; // 初始化摘要为空字符串 + if (cursor.moveToFirst()) { // 如果有记录 + snippet = cursor.getString(0); // 获取摘要字符串 + } + cursor.close(); // 关闭游标 + return snippet; // 返回摘要 + } + // 如果未找到记录,抛出异常 + throw new IllegalArgumentException("Note is not found with id: " + noteId); + } + + /** + * 格式化摘要字符串 + * 主要用于去除字符串两端的空白字符,以及截取至第一个换行符之前的内容。 + * + * @param snippet 需要格式化的摘要字符串 + * @return 格式化后的摘要字符串 + */ + public static String getFormattedSnippet(String snippet) { + // 如果摘要字符串不为空,进行格式化处理 + if (snippet != null) { + snippet = snippet.trim(); // 去除两端的空白字符 + int index = snippet.indexOf('\n'); // 查找第一个换行符的位置 + if (index != -1) { // 如果存在换行符 + snippet = snippet.substring(0, index); // 截取至第一个换行符之前的内容 + } + } + return snippet; // 返回格式化后的摘要字符串 + } +} + diff --git a/src/tool/GTaskStringUtils.java b/src/tool/GTaskStringUtils.java new file mode 100644 index 0000000..aeaa30d --- /dev/null +++ b/src/tool/GTaskStringUtils.java @@ -0,0 +1,64 @@ +/* + * GTaskStringUtils 类定义 + * 该类提供了一系列与 GTask 相关的字符串常量,用于在操作 GTask 数据时标识各种 JSON 属性。 + */ +package net.micode.notes.tool; // 定义该类所在的包路径 + + +// 定义 GTaskStringUtils 类 +public class GTaskStringUtils { + + // GTask JSON 对象中各种属性的键名 + public final static String GTASK_JSON_ACTION_ID = "action_id"; // 动作 ID + public final static String GTASK_JSON_ACTION_LIST = "action_list"; // 动作列表 + public final static String GTASK_JSON_ACTION_TYPE = "action_type"; // 动作类型 + public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; // 创建动作类型 + public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; // 获取所有动作类型 + public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; // 移动动作类型 + public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; // 更新动作类型 + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; // 创建者 ID + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; // 子实体 + public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; // 客户端版本 + public final static String GTASK_JSON_COMPLETED = "completed"; // 完成状态 + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; // 当前列表 ID + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; // 默认列表 ID + public final static String GTASK_JSON_DELETED = "deleted"; // 删除状态 + public final static String GTASK_JSON_DEST_LIST = "dest_list"; // 目标列表 + public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; // 目标父实体 + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; // 目标父实体类型 + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; // 实体增量 + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型 + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; // 获取已删除项 + public final static String GTASK_JSON_ID = "id"; // ID + public final static String GTASK_JSON_INDEX = "index"; // 索引 + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间 + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; // 最新同步点 + public final static String GTASK_JSON_LIST_ID = "list_id"; // 列表 ID + public final static String GTASK_JSON_LISTS = "lists"; // 列表集合 + public final static String GTASK_JSON_NAME = "name"; // 名称 + public final static String GTASK_JSON_NEW_ID = "new_id"; // 新 ID + public final static String GTASK_JSON_NOTES = "notes"; // 备注 + public final static String GTASK_JSON_PARENT_ID = "parent_id"; // 父 ID + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; // 前一个兄弟 ID + public final static String GTASK_JSON_RESULTS = "results"; // 结果 + public final static String GTASK_JSON_SOURCE_LIST = "source_list"; // 源列表 + public final static String GTASK_JSON_TASKS = "tasks"; // 任务集合 + public final static String GTASK_JSON_TYPE = "type"; // 类型 + public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; // 类型:组 + public final static String GTASK_JSON_TYPE_TASK = "TASK"; // 类型:任务 + public final static String GTASK_JSON_USER = "user"; // 用户 + + // MIUI 笔记相关的文件夹前缀和元数据键名 + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; // MIUI 笔记文件夹前缀 + public final static String FOLDER_DEFAULT = "Default"; // 默认文件夹名 + public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话笔记文件夹名 + public final static String FOLDER_META = "METADATA"; // 元数据文件夹名 + + // 元数据头部键名 + public final static String META_HEAD_GTASK_ID = "meta_gid"; // 元数据头部:GTask ID + public final static String META_HEAD_NOTE = "meta_note"; // 元数据头部:笔记内容 + public final static String META_HEAD_DATA = "meta_data"; // 元数据头部:元数据 + + // 元数据笔记名称,不可更新和删除 + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; // 不可更新和删除的元数据名称 +} diff --git a/src/tool/ResourceParser.java b/src/tool/ResourceParser.java new file mode 100644 index 0000000..8a4f353 --- /dev/null +++ b/src/tool/ResourceParser.java @@ -0,0 +1,212 @@ +/* + * ResourceParser 类用于管理与应用资源相关的各种静态方法和常量。 + */ +package net.micode.notes.tool; + +// 导入 Android 的 Context 和 PreferenceManager 类,用于访问应用的上下文和首选项 +import android.content.Context; +import android.preference.PreferenceManager; + +// 导入应用资源和偏好设置相关的类 +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +// 定义 ResourceParser 类 +public class ResourceParser { + + // 定义笔记背景颜色的常量 + public static final int YELLOW = 0; // 黄色背景 + public static final int BLUE = 1; // 蓝色背景 + public static final int WHITE = 2; // 白色背景 + public static final int GREEN = 3; // 绿色背景 + public static final int RED = 4; // 红色背景 + + // 默认背景颜色 + public static final int BG_DEFAULT_COLOR = YELLOW; + + // 定义文本大小的常量 + public static final int TEXT_SMALL = 0; // 小号字体 + public static final int TEXT_MEDIUM = 1; // 中号字体 + public static final int TEXT_LARGE = 2; // 大号字体 + public static final int TEXT_SUPER = 3; // 特大号字体 + + // 默认字体大小 + public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + + /** + * 笔记背景资源类,提供获取不同背景资源的方法。 + */ + public static class NoteBgResources { + // 编辑状态下的背景资源数组 + private final static int[] BG_EDIT_RESOURCES = new int[]{ + R.drawable.edit_yellow, // 黄色背景资源 + R.drawable.edit_blue, // 蓝色背景资源 + R.drawable.edit_white, // 白色背景资源 + R.drawable.edit_green, // 绿色背景资源 + R.drawable.edit_red // 红色背景资源 + }; + + // 编辑状态下的标题背景资源数组 + private final static int[] BG_EDIT_TITLE_RESOURCES = new int[]{ + R.drawable.edit_title_yellow, // 黄色标题背景资源 + R.drawable.edit_title_blue, // 蓝色标题背景资源 + R.drawable.edit_title_white, // 白色标题背景资源 + R.drawable.edit_title_green, // 绿色标题背景资源 + R.drawable.edit_title_red // 红色标题背景资源 + }; + + // 根据id获取编辑状态下的背景资源 + public static int getNoteBgResource(int id) { + return BG_EDIT_RESOURCES[id]; + } + + // 根据id获取编辑状态下的标题背景资源 + public static int getNoteTitleBgResource(int id) { + return BG_EDIT_TITLE_RESOURCES[id]; + } + } + + /** + * 获取默认笔记背景id。 + * + * @param context 上下文对象,用于访问SharedPreferences。 + * @return 如果用户设置了背景颜色,则返回一个随机背景颜色id;否则返回默认背景颜色id。 + */ + public static int getDefaultBgId(Context context) { + // 检查用户是否在设置中启用了背景颜色选项 + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + // 返回一个随机背景颜色id + return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); + } else { + // 返回默认背景颜色id + return BG_DEFAULT_COLOR; + } + } + + /** + * 笔记列表项背景资源类,提供获取不同背景资源的方法。 + */ + public static class NoteItemBgResources { + // 第一个列表项的背景资源数组 + private final static int[] BG_FIRST_RESOURCES = new int[]{ + R.drawable.list_yellow_up, // 黄色第一个项背景 + R.drawable.list_blue_up, // 蓝色第一个项背景 + R.drawable.list_white_up, // 白色第一个项背景 + R.drawable.list_green_up, // 绿色第一个项背景 + R.drawable.list_red_up // 红色第一个项背景 + }; + + // 普通列表项的背景资源数组 + private final static int[] BG_NORMAL_RESOURCES = new int[]{ + R.drawable.list_yellow_middle, // 黄色普通项背景 + R.drawable.list_blue_middle, // 蓝色普通项背景 + R.drawable.list_white_middle, // 白色普通项背景 + R.drawable.list_green_middle, // 绿色普通项背景 + R.drawable.list_red_middle // 红色普通项背景 + }; + + // 最后一个列表项的背景资源数组 + private final static int[] BG_LAST_RESOURCES = new int[]{ + R.drawable.list_yellow_down, // 黄色最后项背景 + R.drawable.list_blue_down, // 蓝色最后项背景 + R.drawable.list_white_down, // 白色最后项背景 + R.drawable.list_green_down, // 绿色最后项背景 + R.drawable.list_red_down // 红色最后项背景 + }; + + // 单个列表项的背景资源数组 + private final static int[] BG_SINGLE_RESOURCES = new int[]{ + R.drawable.list_yellow_single, // 黄色单项背景 + R.drawable.list_blue_single, // 蓝色单项背景 + R.drawable.list_white_single, // 白色单项背景 + R.drawable.list_green_single, // 绿色单项背景 + R.drawable.list_red_single // 红色单项背景 + }; + + // 获取第一个列表项的背景资源 + public static int getNoteBgFirstRes(int id) { + return BG_FIRST_RESOURCES[id]; + } + + // 获取最后一个列表项的背景资源 + public static int getNoteBgLastRes(int id) { + return BG_LAST_RESOURCES[id]; + } + + // 获取单个列表项的背景资源 + public static int getNoteBgSingleRes(int id) { + return BG_SINGLE_RESOURCES[id]; + } + + // 获取普通列表项的背景资源 + public static int getNoteBgNormalRes(int id) { + return BG_NORMAL_RESOURCES[id]; + } + + // 获取文件夹背景资源 + public static int getFolderBgRes() { + return R.drawable.list_folder; + } + } + + /** + * 小部件背景资源类,提供获取小部件背景资源的方法。 + */ + public static class WidgetBgResources { + // 2x 小部件背景资源数组 + private final static int[] BG_2X_RESOURCES = new int[]{ + R.drawable.widget_2x_yellow, // 黄色2x小部件背景 + R.drawable.widget_2x_blue, // 蓝色2x小部件背景 + R.drawable.widget_2x_white, // 白色2x小部件背景 + R.drawable.widget_2x_green, // 绿色2x小部件背景 + R.drawable.widget_2x_red // 红色2x小部件背景 + }; + + // 根据id获取2x小部件的背景资源 + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + // 4x 小部件背景资源数组 + private final static int[] BG_4X_RESOURCES = new int[]{ + R.drawable.widget_4x_yellow, // 黄色4x小部件背景 + R.drawable.widget_4x_blue, // 蓝色4x小部件背景 + R.drawable.widget_4x_white, // 白色4x小部件背景 + R.drawable.widget_4x_green, // 绿色4x小部件背景 + R.drawable.widget_4x_red // 红色4x小部件背景 + }; + + // 根据id获取4x小部件的背景资源 + public static int getWidget4xBgResource(int id) { + return BG_4X_RESOURCES[id]; + } + } + + /** + * 文本外观资源类,提供获取不同文本外观资源的方法。 + */ + public static class TextAppearanceResources { + // 文本外观资源数组 + private final static int[] TEXTAPPEARANCE_RESOURCES = new int[]{ + R.style.TextAppearanceNormal, // 正常文本样式 + R.style.TextAppearanceMedium, // 中等文本样式 + R.style.TextAppearanceLarge, // 大号文本样式 + R.style.TextAppearanceSuper // 特大号文本样式 + }; + + // 根据id获取文本外观资源 + public static int getTexAppearanceResource(int id) { + // 如果id超出资源数组范围,返回默认字体大小 + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; + } + return TEXTAPPEARANCE_RESOURCES[id]; + } + + // 获取文本外观资源的数量 + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +} diff --git a/src/ui/AlarmAlertActivity.java b/src/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..3c77e81 --- /dev/null +++ b/src/ui/AlarmAlertActivity.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.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; +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.tool.DataUtils; + +import java.io.IOException; + + +/* + * AlarmAlertActivity 类用于处理提醒通知的界面和声音播放。 + * 当一个笔记的提醒时间到达时,这个活动会被启动,显示提醒信息并播放提醒声音。 + */ +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 笔记的ID + private long mNoteId; + // 笔记内容的简短预览 + private String mSnippet; + // 预览文本的最大长度 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // 用于播放提醒声音的MediaPlayer对象 + MediaPlayer mPlayer; + + /* + * onCreate 方法初始化活动,设置窗口特性,根据传入的Intent获取笔记ID和简短内容, + * 并根据情况显示对话框和播放声音。 + */ + @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中获取笔记ID和简短内容 + 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 如果屏幕已打开则返回true,否则返回false。 + */ + 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 e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (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); + } + + /* + * 点击对话框按钮的响应处理。 + * 根据点击的按钮启动编辑笔记的活动或结束当前活动。 + */ + 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; + } + } + + /* + * 对话框关闭时的处理。 + * 停止播放提醒声音,结束当前活动。 + */ + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + /* + * 停止播放提醒声音。 + * 如果MediaPlayer对象不为空,则停止播放并释放资源。 + */ + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} diff --git a/src/ui/AlarmInitReceiver.java b/src/ui/AlarmInitReceiver.java new file mode 100644 index 0000000..8da4ef8 --- /dev/null +++ b/src/ui/AlarmInitReceiver.java @@ -0,0 +1,71 @@ +/* + * 该类是广播接收器,用于在应用启动时初始化提醒设置。 + * 当系统启动时,它会检查数据库中所有设置了提醒的笔记,并为每个笔记设置相应的提醒。 + */ +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class AlarmInitReceiver extends BroadcastReceiver { + + // 查询笔记时需要的列 + private static final String[] PROJECTION = new String[]{ + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + // 列的索引 + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; + + /** + * 当接收到广播时执行的操作。主要用于设置所有已记录的提醒时间。 + * + * @param context 上下文,提供访问应用全局功能的入口。 + * @param intent 携带了触发该接收器的广播信息。 + */ + @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,用于在提醒时间触发AlarmReceiver + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建PendingIntent,它是一个延迟的意图,可以在特定时间由系统触发 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取AlarmManager服务,用于设置提醒 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + // 设置提醒 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + // 关闭Cursor,释放资源 + c.close(); + } + } +} + diff --git a/src/ui/AlarmReceiver.java b/src/ui/AlarmReceiver.java new file mode 100644 index 0000000..42d7313 --- /dev/null +++ b/src/ui/AlarmReceiver.java @@ -0,0 +1,30 @@ +/* + * AlarmReceiver类 - 用于处理闹钟广播接收 + * 当接收到闹钟相关的广播时,该类会启动一个指定的Activity + * + * extends BroadcastReceiver: 继承自Android的BroadcastReceiver类 + */ +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class AlarmReceiver extends BroadcastReceiver { + /* + * onReceive方法 - 系统调用的接收广播的方法 + * 当接收到广播时,该方法会被调用,然后启动AlarmAlertActivity + * + * @param context 上下文对象,提供了调用环境的信息 + * @param intent 包含广播的内容 + */ + @Override + public void onReceive(Context context, Intent intent) { + // 设置Intent的类,以便启动AlarmAlertActivity + intent.setClass(context, AlarmAlertActivity.class); + // 添加标志,表示在一个新的任务中启动Activity + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 根据设置的Intent启动Activity + context.startActivity(intent); + } +} diff --git a/src/ui/DateTimePicker.java b/src/ui/DateTimePicker.java new file mode 100644 index 0000000..aea9538 --- /dev/null +++ b/src/ui/DateTimePicker.java @@ -0,0 +1,583 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.text.DateFormatSymbols; +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; + +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; + // 24小时制小时选择器的最小值 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + // 24小时制小时选择器的最大值 + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + // 12小时制小时选择器的最小值 + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + // 12小时制小时选择器的最大值 + 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; + + // 当前是否为24小时制视图 + 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) { + // 处理12小时制下的日期变化和上下午切换 + 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 { + // 处理24小时制下的日期变化 + 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)); + } + } + }; + + + // 分别为分钟和AM/PM选择器监听器设置匿名内部类,实现数值变化时的处理逻辑。 + 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; + } + // 根据偏移量更新日期和小时选择器,并检查是否需要切换AM/PM + 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) { + // 切换AM/PM状态,并更新日期和AM/PM选择器 + 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)); + } + + // 构造函数:指定是否使用24小时制视图 + 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); + + // 初始化日期、小时、分钟和AM/PM选择器,并设置相应的监听器 + 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); + + // 更新控件至初始状态 + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); + + // 设置为当前时间 + setCurrentDate(date); + + setEnabled(isEnabled()); + + // 设置内容描述 + mInitialising = false; + } + + + /** + * 设置控件的启用状态。 + * 如果当前状态与传入状态相同,则不进行任何操作。 + * 启用或禁用日期和时间选择器,并更新内部启用状态。 + * + * @param enabled 控件是否启用 + */ + @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; + } + + /** + * 获取控件的启用状态。 + * + * @return 控件是否启用 + */ + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * 获取当前日期的时间戳(毫秒)。 + * + * @return 当前日期的毫秒时间戳 + */ + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + /** + * 设置当前日期。 + * 根据传入的毫秒时间戳更新日期选择器的值。 + * + * @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)); + } + + /** + * 设置当前日期和时间。 + * 分别设置年、月、日、时和分。 + * + * @param year 当前年份 + * @param month 当前月份 + * @param dayOfMonth 当前日 + * @param hourOfDay 当前小时 + * @param minute 当前分钟 + */ + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 分别设置年、月、日、时和分 + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); + } + + + /** + * 获取当前年份 + * + * @return 当前的年份 + */ + public int getCurrentYear() { + return mDate.get(Calendar.YEAR); + } + + /** + * 设置当前年份 + * + * @param year 当前的年份 + */ + public void setCurrentYear(int year) { + // 如果不是初始化状态并且设置的年份与当前年份相同,则直接返回 + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); // 更新日期控件 + onDateTimeChanged(); // 触发日期时间改变事件 + } + + /** + * 获取当前月份 + * + * @return 当前的月份(从0开始) + */ + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + /** + * 设置当前月份 + * + * @param month 当前的月份(从0开始) + */ + public void setCurrentMonth(int month) { + // 如果不是初始化状态并且设置的月份与当前月份相同,则直接返回 + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); // 更新日期控件 + onDateTimeChanged(); // 触发日期时间改变事件 + } + + /** + * 获取当前日期(月中的天数) + * + * @return 当前的日期(月中的天数) + */ + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + /** + * 设置当前日期(月中的天数) + * + * @param dayOfMonth 当前的日期(月中的天数) + */ + public void setCurrentDay(int dayOfMonth) { + // 如果不是初始化状态并且设置的日期与当前日期相同,则直接返回 + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); // 更新日期控件 + onDateTimeChanged(); // 触发日期时间改变事件 + } + + /** + * 获取当前小时(24小时制),范围为(0~23) + * + * @return 当前的小时(24小时制) + */ + public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); + } + + /** + * 获取当前小时,根据是否为24小时制返回不同的值。 + * 如果是24小时制,与{@link #getCurrentHourOfDay()}返回相同结果; + * 否则,将小时转换为12小时制,并考虑上午/下午。 + * + * @return 当前的小时(根据视图模式可能是12小时制) + */ + private int getCurrentHour() { + if (mIs24HourView) { + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + // 转换为12小时制 + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } + } + + + /** + * 设置当前小时(24小时制),范围为(0~23) + * + * @param hourOfDay 当前小时数 + */ + public void setCurrentHour(int hourOfDay) { + // 如果在初始化中或者小时未改变,则直接返回 + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + // 如果不是24小时视图,则调整小时数并更新AM/PM控制 + 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(); + } + mHourSpinner.setValue(hourOfDay); + onDateTimeChanged(); + } + + /** + * 获取当前分钟数 + * + * @return 当前分钟数 + */ + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + /** + * 设置当前分钟数 + * + * @param minute 当前分钟数值 + */ + public void setCurrentMinute(int minute) { + // 如果在初始化中或者分钟数未改变,则直接返回 + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); + } + + /** + * 获取当前是否为24小时视图 + * + * @return 如果是24小时视图返回true,否则返回false + */ + public boolean is24HourView() { + return mIs24HourView; + } + + /** + * 设置当前视图为24小时制还是AM/PM制 + * + * @param is24HourView 如果为true表示24小时制,false表示AM/PM制 + */ + 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(); + } + + /** + * 根据当前是否为24小时视图来更新AM/PM控制组件的显示和值 + */ + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + /** + * 根据当前是否为24小时视图来更新小时控制组件的最小值和最大值 + */ + 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); + } + } + + /** + * 设置点击“设置”按钮时的回调接口 + * + * @param callback 回调接口实例,如果为null则不执行任何操作 + */ + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; + } + + /** + * 当日期时间被改变时调用此方法,如果设置了日期时间改变监听器,则触发监听器的回调方法 + */ + private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} diff --git a/src/ui/DateTimePickerDialog.java b/src/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..2f9f8a9 --- /dev/null +++ b/src/ui/DateTimePickerDialog.java @@ -0,0 +1,121 @@ +/* + * DateTimePickerDialog类提供了一个日期和时间选择器对话框。 + * 用户可以选择一个日期和时间,然后通过监听器回调返回选择的值。 + */ +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 当前选择的日期和时间 + private Calendar mDate = Calendar.getInstance(); + // 用于指示日期时间选择器是否使用24小时制 + private boolean mIs24HourView; + // 日期时间设置监听器,用于处理日期时间选择后的回调 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 日期时间选择器视图 + private DateTimePicker mDateTimePicker; + + /** + * 日期时间设置监听器接口。 + * 实现此接口的类需要提供OnDateTimeSet方法来处理日期时间被设置的事件。 + */ + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + /** + * 构造函数初始化日期时间选择器对话框。 + * + * @param context 上下文对象,通常是指Activity。 + * @param date 初始显示的日期时间值。 + */ + public DateTimePickerDialog(Context context, long date) { + super(context); + mDateTimePicker = new DateTimePicker(context); + setView(mDateTimePicker); + // 设置日期时间改变的监听器 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + 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); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + // 更新对话框的标题显示 + 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小时制。 + * + * @param is24HourView 是否使用24小时制。 + */ + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + /** + * 设置日期时间被设置时的监听器。 + * + * @param callBack 日期时间设置监听器对象。 + */ + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + /** + * 更新对话框标题以显示当前选择的日期和时间。 + * + * @param date 当前选择的日期时间值。 + */ + private void updateTitle(long date) { + // 根据是否使用24小时制来格式化日期时间显示 + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + /** + * 点击按钮时的处理逻辑。 + * 如果设置了日期时间设置监听器,则调用其OnDateTimeSet方法,传入当前选择的日期时间值。 + * + * @param arg0 对话框对象。 + * @param arg1 按钮标识。 + */ + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} diff --git a/src/ui/DropdownMenu.java b/src/ui/DropdownMenu.java new file mode 100644 index 0000000..8330499 --- /dev/null +++ b/src/ui/DropdownMenu.java @@ -0,0 +1,73 @@ +/* + * DropdownMenu类用于创建和管理一个下拉菜单。 + * 该类封装了一个Button和一个PopupMenu,通过点击Button来显示下拉菜单。 + */ +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + private Button mButton; // 弹出下拉菜单的按钮 + private PopupMenu mPopupMenu; // 弹出的下拉菜单 + private Menu mMenu; // 下拉菜单的项目集合 + + /** + * DropdownMenu的构造函数。 + * + * @param context 上下文对象,通常是指Activity。 + * @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); // 创建PopupMenu实例 + mMenu = mPopupMenu.getMenu(); // 获取菜单项的集合 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 加载菜单项 + // 设置按钮点击事件,点击后显示下拉菜单 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + /** + * 设置下拉菜单项的点击事件监听器。 + * + * @param listener PopupMenu的OnMenuItemClickListener,用于监听菜单项的点击事件。 + */ + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + /** + * 根据ID查找菜单项。 + * + * @param id 菜单项的ID。 + * @return 返回找到的MenuItem对象,如果未找到则返回null。 + */ + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + /** + * 设置按钮的标题。 + * + * @param title 按钮要显示的标题。 + */ + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/src/ui/FoldersListAdapter.java b/src/ui/FoldersListAdapter.java new file mode 100644 index 0000000..2f461ca --- /dev/null +++ b/src/ui/FoldersListAdapter.java @@ -0,0 +1,110 @@ +/* + * FoldersListAdapter 类 + * 用于管理和适配文件夹列表的适配器,继承自 CursorAdapter。 + */ + +package net.micode.notes.ui; + +// 导入相关类 + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class FoldersListAdapter extends CursorAdapter { + // 查询时使用的列名数组 + public static final String[] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 列名数组中的索引常量 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + /* + * 构造函数 + * @param context 上下文对象,通常指Activity或Application对象。 + * @param c 数据源游标Cursor。 + */ + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + } + + /* + * 创建新的列表项View + * @param context 上下文对象。 + * @param cursor 当前数据项的游标。 + * @param parent 视图的父容器。 + * @return 返回新创建的列表项View。 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + /* + * 绑定数据到已有的View上 + * @param view 要绑定数据的视图。 + * @param context 上下文对象。 + * @param cursor 当前数据项的游标。 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 根据文件夹ID判断是根文件夹还是普通文件夹,并设置文件夹名称 + 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); + } + + /* + * FolderListItem 内部类 + * 用于表示文件夹列表中的一个项的视图类。 + */ + private class FolderListItem extends LinearLayout { + 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); // 获取文件夹名称的视图 + } + + /* + * 绑定数据到视图上 + * @param name 要显示的文件夹名称。 + */ + public void bind(String name) { + mName.setText(name); + } + } + +} diff --git a/src/ui/NoteEditActivity.java b/src/ui/NoteEditActivity.java new file mode 100644 index 0000000..76c4c22 --- /dev/null +++ b/src/ui/NoteEditActivity.java @@ -0,0 +1,1141 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +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.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + /** + * 头部视图的ViewHolder类,用于存储头部视图中的UI组件引用。 + */ + 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); + sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED); + sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE); + sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN); + 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); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + 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); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + 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); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + private static final String TAG = "NoteEditActivity"; // 日志标签 + + private HeadViewHolder mNoteHeaderHolder; // 头部视图的ViewHolder + + 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 int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷图标标题的最大长度 + + public static final String TAG_CHECKED = String.valueOf('\u221A'); // 标记为已检查的字符串 + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 标记为未检查的字符串 + + private LinearLayout mEditTextList; // 编辑文本列表 + + private String mUserQuery; // 用户查询字符串 + private Pattern mPattern; // 正则表达式模式 + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); // 设置活动的视图布局 + + // 检查实例状态是否被保存,如果未保存且初始化活动状态失败,则结束该活动 + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); // 初始化资源 + } + + /** + * 当活动被系统销毁后,为了恢复之前的状态,此方法会被调用。 + * 主要用于处理活动重新创建时的数据恢复。 + * + * @param savedInstanceState 包含之前保存状态的Bundle,如果活动之前保存了状态,这里会传入非空的Bundle。 + */ + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 检查是否有保存的状态并且包含必要的UID信息,用于恢复活动状态 + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + // 使用intent尝试恢复活动状态,如果失败则结束该活动 + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); // 日志记录,表示活动状态正在从被杀死的状态恢复 + } + } + + + /** + * 初始化活动状态,根据传入的Intent确定是查看笔记、新建笔记还是编辑笔记。 + * + * @param intent 传入的Intent,包含了动作类型和相关数据。 + * @return boolean 返回true表示成功初始化活动状态,false表示初始化失败。 + */ + private boolean initActivityState(Intent intent) { + // 如果用户指定的是查看笔记的动作但未提供笔记ID,则跳转到笔记列表活动 + mWorkingNote = null; + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + // 从搜索结果开始 + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + // 检查指定的笔记在数据库中是否可见 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + // 加载指定ID的笔记 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } + } + // 隐藏软键盘 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + } else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // 处理新建或编辑笔记的情况 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // 解析通话记录笔记 + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate != 0 && phoneNumber != null) { + // 根据电话号码和通话日期尝试获取已有笔记ID + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + // 加载该笔记 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load call note failed with note id" + noteId); + finish(); + return false; + } + } else { + // 创建新的通话记录笔记 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, + widgetType, bgResId); + mWorkingNote.convertToCallNote(phoneNumber, callDate); + } + } else { + // 创建普通空笔记 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, + bgResId); + } + + // 显示软键盘 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } else { + // 不支持的Intent动作 + Log.e(TAG, "Intent not specified action, should not support"); + finish(); + return false; + } + // 设置笔记状态改变的监听器 + mWorkingNote.setOnSettingStatusChangedListener(this); + return true; + } + + + /** + * 当Activity恢复到前台时调用。 + * 主要负责初始化笔记界面。 + */ + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + /** + * 初始化笔记界面的函数。 + * 该函数设置笔记编辑器的外观,根据笔记类型切换到相应的模式,设置背景和头部信息, + * 以及处理提醒头部的显示。 + */ + private void initNoteScreen() { + // 设置编辑器的文本外观 + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + // 根据当前笔记的类型,切换到列表模式或高亮查询结果模式 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + // 隐藏所有背景选择器 + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + // 设置标题和编辑区域的背景 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + // 设置修改时间 + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + // 显示提醒头部信息(当前禁用,因DateTimePicker未准备好) + showAlertHeader(); + } + + /** + * 显示提醒头部信息的方法。 + * 如果当前笔记设置了提醒,该方法将根据提醒时间显示相对的时间或者过期信息。 + */ + private void showAlertHeader() { + // 处理提醒显示 + if (mWorkingNote.hasClockAlert()) { + long time = System.currentTimeMillis(); + // 如果提醒时间已过,显示提醒已过期的信息 + if (time > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + // 否则,显示相对时间 + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + // 设置提醒视图可见 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + // 如果没有设置提醒,隐藏提醒视图 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + } + } + + /** + * 当Activity接收到新的Intent时调用。 + * 用于根据新的Intent重新初始化Activity状态。 + * + * @param intent 新的Intent + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + /** + * 保存Activity状态时调用。 + * 用于保存当前编辑的笔记,如果该笔记还未保存到数据库,则首先保存它。 + * 并且保存当前笔记的ID到Bundle中。 + * + * @param outState 用于保存Activity状态的Bundle + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 如果当前编辑的笔记不存在于数据库中,即还未保存,先保存它 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + // 保存笔记ID + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); + } + + + /** + * 分发触摸事件。如果触摸事件不在指定视图范围内,则隐藏该视图。 + * + * @param ev 触摸事件 + * @return 如果事件被消费则返回true,否则返回false + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // 如果背景颜色选择器可见且不在触摸范围内,则隐藏它并消费事件 + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + // 如果字体大小选择器可见且不在触摸范围内,则隐藏它并消费事件 + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + // 如果上述条件都不满足,则将事件传递给父类处理 + return super.dispatchTouchEvent(ev); + } + + /** + * 判断触摸点是否在指定视图范围内。 + * + * @param view 视图 + * @param ev 触摸事件 + * @return 如果触摸点在视图范围内则返回true,否则返回false + */ + private boolean inRangeOfView(View view, MotionEvent ev) { + int[] location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0]; + int y = location[1]; + // 判断触摸点是否在视图外 + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + return true; + } + + /** + * 初始化资源,包括视图和偏好设置。 + */ + private void initResources() { + // 初始化头部视图和相关组件 + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + // 初始化编辑器和相关组件 + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + // 设置背景选择器按钮点击监听器 + for (int id : sBgSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } + + mFontSizeSelector = findViewById(R.id.font_size_selector); + // 设置字体大小选择器按钮点击监听器 + for (int id : sFontSizeBtnsMap.keySet()) { + View view = findViewById(id); + view.setOnClickListener(this); + } + ; + // 从偏好设置中读取字体大小 + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + // 修复存储在偏好设置中的字体大小资源ID的错误 + if (mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + // 初始化编辑列表 + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + } + + /** + * 暂停时保存笔记数据并清除设置状态。 + */ + @Override + protected void onPause() { + super.onPause(); + // 保存笔记数据 + if (saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + // 清除设置状态 + clearSettingState(); + } + + /** + * 更新小部件显示。 + */ + private void updateWidget() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据小部件类型设置对应的类 + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + // 设置小部件ID + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[]{ + mWorkingNote.getWidgetId() + }); + + // 发送广播更新小部件 + sendBroadcast(intent); + // 设置结果为OK + setResult(RESULT_OK, intent); + } + + + /** + * 点击事件的处理函数。 + * + * @param v 被点击的视图对象。 + */ + 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); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + // 如果点击的是背景色板上的颜色 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); // 隐藏当前选择的背景颜色 + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); // 更新笔记的背景颜色 + mNoteBgColorSelector.setVisibility(View.GONE); // 隐藏背景颜色选择器 + } else if (sFontSizeBtnsMap.containsKey(id)) { + // 如果点击的是字体大小按钮 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); // 隐藏当前选择的字体大小 + mFontSizeId = sFontSizeBtnsMap.get(id); // 更新选择的字体大小 + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); // 保存选择的字体大小到SharedPreferences + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); // 设置新选择的字体大小按钮为可见 + // 根据当前笔记是否为清单模式,进行相应的文本更新 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); // 隐藏字体大小选择器 + } + } + + /** + * 按下返回键时的处理函数。 + */ + @Override + public void onBackPressed() { + // 尝试清除设置状态,如果成功,则不执行保存笔记操作 + if (clearSettingState()) { + return; + } + // 保存笔记并执行父类的onBackPressed()方法(结束当前Activity) + saveNote(); + super.onBackPressed(); + } + + /** + * 尝试清除设置状态(背景颜色选择或字体大小选择)。 + * + * @return 如果成功清除设置状态,返回true;否则返回false。 + */ + private boolean clearSettingState() { + // 如果背景颜色选择器可见,则隐藏它并返回true + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { // 如果字体大小选择器可见,则隐藏它并返回true + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return false; // 没有可见的设置状态需要清除,返回false + } + + /** + * 当背景颜色发生变化时的处理函数。 + */ + public void onBackgroundColorChanged() { + // 根据当前笔记的背景颜色,设置对应的色板按钮可见,并更新编辑器及标题栏的背景颜色 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + } + + /** + * 准备选项菜单的函数。 + * + * @param menu 选项菜单对象。 + * @return 总是返回true。 + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // 如果Activity正在结束,则不进行任何操作 + 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; + } + + + /** + * 处理选项菜单项的选择事件。 + * + * @param item 选中的菜单项 + * @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(); + } + + /** + * 分享笔记到支持 {@link Intent#ACTION_SEND} 操作和 {@text/plain} 类型的应用。 + * + * @param context 上下文 + * @param info 要分享的信息 + */ + 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); + } + + /** + * 首先保存当前正在编辑的笔记,然后启动一个新的NoteEditActivity用于创建新笔记。 + */ + private void createNewNote() { + // 首先保存当前笔记 + saveNote(); + + // 安全地开始一个新的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); + } + + /** + * 判断当前是否为同步模式。 + * 同步模式是指在设置中配置了同步账户名。 + * + * @return 如果配置了同步账户名返回true,否则返回false。 + */ + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + /** + * 处理时钟提醒变更事件。 + * 首先检查当前笔记是否已保存,未保存则先保存。 + * 如果笔记存在,根据set参数设置或取消提醒。 + * 如果笔记不存在(即无有效ID),记录错误并提示用户输入内容。 + * + * @param date 提醒的日期时间戳 + * @param set 是否设置提醒 + */ + public void onClockAlertChanged(long date, boolean set) { + 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 { + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } + } + + /** + * 更新小部件显示。 + */ + public void onWidgetChanged() { + updateWidget(); + } + + /** + * 当删除某个编辑框中的文本时的处理逻辑。 + * 重新设置后续编辑框的索引,并将删除的文本添加到前一个或当前编辑框中。 + * + * @param index 被删除文本的编辑框索引 + * @param text 被删除的文本内容 + */ + 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); + } + + /** + * 当在编辑框中按下“Enter”键时的处理逻辑。 + * 在列表中添加一个新的编辑框,并重新设置后续编辑框的索引。 + * + * @param index 当前编辑框的索引 + * @param text 当前编辑框中的文本内容 + */ + public void onEditTextEnter(int index, String text) { + 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); + } + } + + + /** + * 切换到列表模式。 + * 将文本分割成多行,并为每行创建一个列表项,展示在编辑文本列表中。 + * + * @param text 要转换成列表模式的文本,每行代表一个列表项。 + */ + 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); + } + + /** + * 高亮显示查询结果。 + * 在给定的文本中,根据用户查询字符串高亮显示匹配的部分。 + * + * @param fullText 完整的文本。 + * @param userQuery 用户的查询字符串。 + * @return 包含高亮显示的文本的Spannable对象。 + */ + 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; + } + + /** + * 创建列表项视图。 + * 为列表模式创建并配置一个包含文本编辑框和复选框的视图。 + * + * @param item 列表项的文本内容。 + * @param index 列表项的索引。 + * @return 配置好的列表项视图。 + */ + 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; + } + + /** + * 根据文本内容是否为空,切换复选框的可见性。 + * + * @param index 列表项索引。 + * @param hasText 列表项是否包含文本。 + */ + 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); + } + } + + /** + * 在切换编辑模式和列表模式时更新UI。 + * + * @param oldMode 旧的编辑模式。 + * @param newMode 新的编辑模式。 + */ + 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); + } + } + + /** + * 根据当前列表项的选中状态,构建并返回工作文本。 + * + * @return 是否有已选中的列表项。 + */ + 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; + } + + /** + * 保存笔记。 + * 更新笔记内容并保存。 + * + * @return 是否成功保存笔记。 + */ + private boolean saveNote() { + getWorkingText(); + boolean saved = mWorkingNote.saveNote(); + if (saved) { + // 设置结果为成功,以便外部调用者知道保存操作的状态 + setResult(RESULT_OK); + } + return saved; + } + + + /** + * 将当前编辑的笔记发送到桌面。首先会检查当前编辑的笔记是否已存在于数据库中, + * 如果不存在,则先保存。如果存在,会创建一个快捷方式放在桌面。 + */ + private void sendToDesktop() { + // 检查当前编辑的笔记是否存在于数据库,若不存在则先保存 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + // 如果笔记存在于数据库(有noteId),则创建快捷方式 + 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 { + // 如果用户未输入任何内容,无法创建快捷方式,提醒用户输入内容 + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } + } + + /** + * 根据笔记内容生成快捷方式的标题。移除内容中的已选和未选标签,并确保标题长度不超过上限。 + * + * @param content 符合快捷方式图标标题要求的笔记内容 + * @return 标题字符串 + */ + 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; + } + + /** + * 显示一个Toast消息。 + * + * @param resId 资源ID,指向要显示的字符串 + */ + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + /** + * 显示一个指定时长的Toast消息。 + * + * @param resId 资源ID,指向要显示的字符串 + * @param duration 显示时长,可以是Toast.LENGTH_SHORT或Toast.LENGTH_LONG + */ + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); + } + +} diff --git a/src/ui/NoteEditText.java b/src/ui/NoteEditText.java new file mode 100644 index 0000000..8e3e59f --- /dev/null +++ b/src/ui/NoteEditText.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +/** + * 自定义的可编辑文本视图,支持文本变化监听、删除和新增文本事件。 + */ +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; // 当前文本视图的索引 + private int mSelectionStartBeforeDelete; // 删除操作前的选择起始位置 + + private static final String SCHEME_TEL = "tel:"; + private static final String SCHEME_HTTP = "http:"; + private static final String SCHEME_EMAIL = "mailto:"; + + // URL方案与对应操作资源ID的映射 + private static final Map sSchemaActionResMap = new HashMap(); + + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * 文本视图变化监听接口。 + */ + public interface OnTextViewChangeListener { + /** + * 当按下删除键且文本为空时,删除当前文本视图。 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键时,新增一个文本视图。 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本变化时,隐藏或显示项目选项。 + */ + void onTextChange(int index, boolean hasText); + } + + private OnTextViewChangeListener mOnTextViewChangeListener; + + /** + * 构造函数,初始化编辑文本视图。 + * + * @param context 上下文对象 + */ + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + /** + * 设置当前文本视图的索引。 + * + * @param index 当前文本视图的索引 + */ + public void setIndex(int index) { + mIndex = index; + } + + /** + * 设置文本视图变化监听器。 + * + * @param listener 文本视图变化监听器 + */ + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + /** + * 构造函数,初始化编辑文本视图。 + * + * @param context 上下文对象 + * @param attrs 属性集 + */ + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + /** + * 构造函数,初始化编辑文本视图。 + * + * @param context 上下文对象 + * @param attrs 属性集 + * @param defStyle 样式 + */ + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * 处理触摸事件,调整光标位置。 + */ + @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(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); + } + + /** + * 处理键盘按下事件。 + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 处理回车键事件,准备新增文本视图 + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + // 记录删除操作前的选择位置 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + /** + * 处理键盘弹起事件。 + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + // 处理删除键事件,若为首个文本且非空,则删除当前文本 + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + // 处理回车键事件,新增文本视图 + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); + } + + /** + * 当焦点变化时,通知文本变化情况。 + */ + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + /** + * 创建上下文菜单,支持点击URL跳转。 + */ + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for (String schema : sSchemaActionResMap.keySet()) { + if (urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 跳转到URL指向的页面 + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} + diff --git a/src/ui/NoteItemData.java b/src/ui/NoteItemData.java new file mode 100644 index 0000000..e0c70c5 --- /dev/null +++ b/src/ui/NoteItemData.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + + +/** + * 代表一个笔记项的数据类,用于存储和管理笔记的各种信息。 + */ +public class NoteItemData { + // 定义查询时要投影的列 + static final String[] PROJECTION = new String[]{ + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + + // 各列数据的索引 + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + // 笔记的各项数据 + private long mId; + private long mAlertDate; + private int mBgColorId; + private long mCreatedDate; + private boolean mHasAttachment; + private long mModifiedDate; + private int mNotesCount; + private long mParentId; + private String mSnippet; + private int mType; + private int mWidgetId; + private int mWidgetType; + private String mName; + private String mPhoneNumber; + + // 用于标识笔记在列表中的位置状态 + private boolean mIsLastItem; + private boolean mIsFirstItem; + private boolean mIsOnlyOneItem; + private boolean mIsOneNoteFollowingFolder; + private boolean mIsMultiNotesFollowingFolder; + + /** + * 根据Cursor数据构造一个NoteItemData对象。 + * + * @param context 上下文对象,用于访问应用全局功能。 + * @param cursor 包含笔记数据的Cursor对象。 + */ + public NoteItemData(Context context, Cursor 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; + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + // 如果是通话记录笔记,尝试获取通话号码和联系人名称 + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) { + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + // 如果没有获取到联系人名称,则默认为空字符串 + if (mName == null) { + mName = ""; + } + checkPostion(cursor); + } + + /** + * 根据当前Cursor位置,更新NoteItemData的状态信息(如是否为列表中的最后一个项目等)。 + * + * @param cursor 包含笔记数据的Cursor对象。 + */ + private void checkPostion(Cursor cursor) { + // 更新位置状态信息 + mIsLastItem = cursor.isLast(); + mIsFirstItem = cursor.isFirst(); + mIsOnlyOneItem = (cursor.getCount() == 1); + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + // 检查当前笔记是否跟随文件夹,并更新相应状态 + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) { + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + mIsOneNoteFollowingFolder = true; + } + } + // 确保Cursor能够回到原来的位置 + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + // 以下为获取NoteItemData各项属性的方法 + + 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; + } + + 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; + } + + public int getBgColorId() { + return mBgColorId; + } + + public long getParentId() { + return mParentId; + } + + public int getNotesCount() { + return mNotesCount; + } + + public long getFolderId() { + return mParentId; + } + + public int getType() { + return mType; + } + + public int getWidgetType() { + return mWidgetType; + } + + 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)); + } + + /** + * 从Cursor中获取笔记的类型。 + * + * @param cursor 包含笔记数据的Cursor对象。 + * @return 笔记的类型。 + */ + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} + diff --git a/src/ui/NoteWidgetProvider.java b/src/ui/NoteWidgetProvider.java new file mode 100644 index 0000000..898de0a --- /dev/null +++ b/src/ui/NoteWidgetProvider.java @@ -0,0 +1,169 @@ +/* + * 注意:此代码段的版权归 MiCode 开源社区所有(www.micode.net) + * 本代码遵循 Apache 2.0 许可证,您可以在 http://www.apache.org/licenses/LICENSE-2.0 查看许可证内容。 + */ + +package net.micode.notes.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +/** + * 笔记小部件提供者抽象类,扩展自AppWidgetProvider,用于管理和更新笔记小部件的内容。 + */ +public abstract class NoteWidgetProvider extends AppWidgetProvider { + // 查询笔记时用到的列名数组 + public static final String[] PROJECTION = new String[]{ + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + // 列的索引常量 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + // 日志标签 + private static final String TAG = "NoteWidgetProvider"; + + /** + * 当小部件被删除时调用,更新数据库中对应小部件的ID为无效ID。 + */ + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[]{String.valueOf(appWidgetIds[i])}); + } + } + + /** + * 根据小部件ID查询对应的笔记信息。 + * + * @param context 上下文 + * @param widgetId 小部件ID + * @return 返回查询到的Cursor对象,包含笔记的摘要、背景ID等信息。 + */ + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[]{String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER)}, + null); + } + + /** + * 更新小部件显示内容的通用方法。 + * + * @param context 上下文 + * @param appWidgetManager AppWidget管理器 + * @param appWidgetIds 小部件ID数组 + */ + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + /** + * 根据是否隐私模式更新小部件显示内容。 + * + * @param context 上下文 + * @param appWidgetManager AppWidget管理器 + * @param appWidgetIds 小部件ID数组 + * @param privacyMode 是否为隐私模式 + */ + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + int bgId = ResourceParser.getDefaultBgId(context); + String snippet = ""; + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + if (c != null) { + c.close(); + } + + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 为小部件的点击事件设置PendingIntent + PendingIntent pendingIntent = null; + if (privacyMode) { + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + /** + * 获取背景资源的ID。 + * + * @param bgId 背景ID + * @return 返回对应的资源ID + */ + protected abstract int getBgResourceId(int bgId); + + /** + * 获取小部件布局的ID。 + * + * @return 返回布局的资源ID + */ + protected abstract int getLayoutId(); + + /** + * 获取小部件的类型。 + * + * @return 返回小部件的类型 + */ + protected abstract int getWidgetType(); +} + diff --git a/src/ui/Notes.java b/src/ui/Notes.java new file mode 100644 index 0000000..226763b --- /dev/null +++ b/src/ui/Notes.java @@ -0,0 +1,273 @@ +/* + * 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.data; + +import android.net.Uri; + +// Notes类定义了与笔记和文件夹相关的常量和数据列接口 +public class Notes { + public static final String AUTHORITY = "micode_notes"; // 用于标识内容提供者的授权名称 + public static final String TAG = "Notes"; // 日志标签 + public static final int TYPE_NOTE = 0; // 笔记类型 + public static final int TYPE_FOLDER = 1; // 文件夹类型 + public static final int TYPE_SYSTEM = 2; // 系统类型 + + /** + * 下面的ID是系统文件夹的标识符 + * {@link Notes#ID_ROOT_FOLDER} 是默认文件夹 + * {@link Notes#ID_TEMPARAY_FOLDER} 是属于没有文件夹的笔记 + * {@link Notes#ID_CALL_RECORD_FOLDER} 是用于存储通话记录的 + */ + public static final int ID_ROOT_FOLDER = 0; // 根文件夹ID + public static final int ID_TEMPARAY_FOLDER = -1; // 临时文件夹ID,用于存放不属于任何文件夹的笔记 + public static final int ID_CALL_RECORD_FOLDER = -2; // 通话记录文件夹ID + public static final int ID_TRASH_FOLER = -3; // 垃圾箱文件夹ID + + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; // 用于Intent的提醒日期额外数据 + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; // 笔记背景色ID + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; // 小部件ID + public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; // 小部件类型 + public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; // 文件夹ID + public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; // 通话日期 + + public static final int TYPE_WIDGET_INVALIDE = -1; // 无效的小部件类型 + public static final int TYPE_WIDGET_2X = 0; // 2x小部件类型 + public static final int TYPE_WIDGET_4X = 1; // 4x小部件类型 + + public static class DataConstants { + public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; // 笔记的内容项类型 + public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; // 通话记录的内容项类型 + } + + /** + * 查询所有笔记和文件夹的Uri + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * 查询数据的Uri + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + // 笔记和文件夹的公共列接口 + public interface NoteColumns { + /** + * 行的唯一ID + *

类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 笔记或文件夹的父ID + *

类型: INTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * 创建日期 + *

类型: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 最后修改日期 + *

类型: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 提醒日期 + *

类型: INTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * 笔记或文件夹的摘要信息 + *

类型: TEXT

+ */ + public static final String SNIPPET = "snippet"; + + /** + * 笔记的小部件ID + *

类型: INTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * 笔记的小部件类型 + *

类型: INTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * 笔记的背景色ID + *

类型: INTEGER (long)

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * 笔记是否有附件 + *

类型: INTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * 笔记数量 + *

类型: INTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * 文件夹类型:0-笔记,1-文件夹 + *

类型: INTEGER

+ */ + public static final String TYPE = "type"; + + /** + * 最后同步ID + *

类型: INTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * 标记本地是否已修改 + *

类型: INTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * 移入临时文件夹前的原始父ID + *

类型: INTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * Google任务ID + *

类型: TEXT

+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * 版本号 + *

类型: INTEGER (long)

+ */ + public static final String VERSION = "version"; + } + + // 数据列接口 + public interface DataColumns { + /** + * 行的唯一ID + *

类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 该项的MIME类型。 + *

类型: TEXT

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * 属于的笔记的引用ID + *

类型: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 创建日期 + *

类型: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 最后修改日期 + *

类型: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 数据内容 + *

类型: TEXT

+ */ + public static final String CONTENT = "content"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储整型数据 + *

类型: INTEGER

+ */ + public static final String DATA1 = "data1"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储整型数据 + *

类型: INTEGER

+ */ + public static final String DATA2 = "data2"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储文本数据 + *

类型: TEXT

+ */ + public static final String DATA3 = "data3"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储文本数据 + *

类型: TEXT

+ */ + public static final String DATA4 = "data4"; + + /** + * 通用数据列,具体含义由{@link #MIME_TYPE}决定,用于存储文本数据 + *

类型: TEXT

+ */ + public static final String DATA5 = "data5"; + } + + // 文本笔记类,实现了DataColumns接口 + public static final class TextNote implements DataColumns { + /** + * 模式,指示文本是否在检查列表模式中 + *

类型: INTEGER 1:检查列表模式 0: 正常模式

+ */ + public static final String MODE = DATA1; + + public static final int MODE_CHECK_LIST = 1; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; // MIME类型定义 + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; // 单项MIME类型定义 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); // 内容URI定义 + } + + // 通话记录笔记类,实现了DataColumns接口 + public static final class CallNote implements DataColumns { + /** + * 通话日期 + *

类型: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; + + /** + * 电话号码 + *

类型: TEXT

+ */ + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; // MIME类型定义 + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; // 单项MIME类型定义 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); // 内容URI定义 + } +} diff --git a/src/ui/NotesDatabaseHelper.java b/src/ui/NotesDatabaseHelper.java new file mode 100644 index 0000000..36973ec --- /dev/null +++ b/src/ui/NotesDatabaseHelper.java @@ -0,0 +1,381 @@ +/* + * 该类为Notes数据库的辅助类,负责管理数据库的创建和版本管理。 + */ +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + + +public class NotesDatabaseHelper extends SQLiteOpenHelper { + // 数据库名称·· + private static final String DB_NAME = "note.db"; + + // 数据库版本号 + private static final int DB_VERSION = 4; + + // 表接口,定义了数据库中的两个表名 + public interface TABLE { + public static final String NOTE = "note"; + + public static final String DATA = "data"; + } + + // 日志标签 + private static final String TAG = "NotesDatabaseHelper"; + + // 单例模式,确保数据库辅助类的唯一实例 + private static NotesDatabaseHelper mInstance; + + // 创建NOTE表的SQL语句 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + // 创建DATA表的SQL语句 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + // 创建DATA表的NOTE_ID索引的SQL语句 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + // 当更新NOTE表中的PARENT_ID字段时,增加目标文件夹的NOTE_COUNT + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + // 当从文件夹移动NOTE时,减少源文件夹的NOTE_COUNT + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + // 当插入新NOTE时,增加目标文件夹的NOTE_COUNT + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + // 当删除NOTE时,减少文件夹的NOTE_COUNT + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + // 当插入DATA时,如果类型为NOTE,则更新关联NOTE的内容 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + // 当更新DATA时,如果类型为NOTE,则更新关联NOTE的内容 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + // 当删除DATA时,如果类型为NOTE,则更新关联NOTE的内容为空 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + // 当删除NOTE时,删除关联的DATA + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 当删除NOTE时,删除属于该NOTE的子NOTE + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 当NOTE移动到回收站文件夹时,将所有子NOTE也移动到回收站 + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 构造函数,私有化以防止外部实例化 + * context 上下文对象,用于访问应用的资源和其他组件 + */ + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + /** + * 创建NOTE表,并重新创建NOTE表的触发器,然后创建系统文件夹 + *db SQLiteDatabase对象,用于执行SQL创建语句 + */ + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); + reCreateNoteTableTriggers(db); + createSystemFolder(db); + Log.d(TAG, "note table has been created"); + } + + /** + * 重新创建笔记表的触发器 + * db SQLiteDatabase 类型,数据库对象 + */ + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + // 删除旧的触发器 + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + // 创建新的触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + /** + * 创建系统文件夹 + * db SQLiteDatabase 类型,数据库对象 + */ + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + // 创建通话记录文件夹 + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建根文件夹(默认文件夹) + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建临时文件夹,用于移动笔记 + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建回收站文件夹 + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 创建数据表 + * db SQLiteDatabase 类型,数据库对象 + */ + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); + reCreateDataTableTriggers(db); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + Log.d(TAG, "data table has been created"); + } + + /** + * 重新创建数据表的触发器 + *db SQLiteDatabase 类型,数据库对象 + */ + private void reCreateDataTableTriggers(SQLiteDatabase db) { + // 删除旧的触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + // 创建新的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + /** + * 获取 NotesDatabaseHelper 的单例对象 + *context Context 类型,应用上下文 + *NotesDatabaseHelper 类型,单例对象 + */ + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + /** + * 创建数据库表 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + + /** + * 升级数据库 + * + * @param db SQLiteDatabase 类型,数据库对象 + * @param oldVersion int 类型,旧版本号 + * @param newVersion int 类型,新版本号 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + // 根据旧版本号逐步升级 + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // 这次升级包括从 v2 到 v3 的升级 + oldVersion++; + } + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + if (oldVersion == 3) { + upgradeToV4(db); + oldVersion++; + } + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + /** + * 从版本1升级到版本2 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + private void upgradeToV2(SQLiteDatabase db) { + // 删除旧表,创建新表 + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + /** + * 从版本2升级到版本3 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + private void upgradeToV3(SQLiteDatabase db) { + // 删除未使用的触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // 添加一个用于 gtask id 的列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // 添加一个回收站系统文件夹 + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 从版本3升级到版本4 + * + * @param db SQLiteDatabase 类型,数据库对象 + */ + private void upgradeToV4(SQLiteDatabase db) { + // 添加版本号列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} \ No newline at end of file diff --git a/src/ui/NotesListActivity.java b/src/ui/NotesListActivity.java new file mode 100644 index 0000000..b53dfee --- /dev/null +++ b/src/ui/NotesListActivity.java @@ -0,0 +1,1305 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 定义文件夹中笔记列表查询的标记 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + + // 定义文件夹列表查询的标记 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + // 菜单中删除文件夹的选项 + private static final int MENU_FOLDER_DELETE = 0; + + // 菜单中查看文件夹的选项 + private static final int MENU_FOLDER_VIEW = 1; + + // 菜单中更改文件夹名称的选项 + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + // 首次使用应用时,添加介绍信息的偏好设置键 + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + // 列表编辑状态的枚举,包括笔记列表、子文件夹和通话记录文件夹 + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + } + + ; + + // 当前编辑状态 + private ListEditState mState; + + // 后台查询处理器 + private BackgroundQueryHandler mBackgroundQueryHandler; + + // 笔记列表的适配器 + private NotesListAdapter mNotesListAdapter; + + // 笔记列表视图 + private ListView mNotesListView; + + // 添加新笔记的按钮 + private Button mAddNewNote; + + // 是否分发事件的标志 + private boolean mDispatch; + + // 触摸点的原始Y坐标 + private int mOriginY; + + // 分发事件时的Y坐标 + private int mDispatchY; + + // 标题栏文本视图 + private TextView mTitleBar; + + // 当前文件夹的ID + 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 final static int REQUEST_CODE_OPEN_NODE = 102; + // 新建节点请求代码 + private final static int REQUEST_CODE_NEW_NODE = 103; + + /** + * 在活动创建时调用,用于初始化资源和设置应用信息。 + * + * @param savedInstanceState 如果活动之前被销毁,这参数包含之前的状态。如果活动没被销毁之前,这参数是null。 + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + // 用户首次使用时插入介绍信息 + setAppInfoFromRawRes(); + } + + /** + * 处理从其他活动返回的结果。 + * + * @param requestCode 启动其他活动时传入的请求代码。 + * @param resultCode 其他活动返回的结果代码。 + * @param data 其他活动返回的数据。 + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 如果返回结果为OK且请求代码为打开节点或新建节点,则刷新列表 + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + + /** + * 从原始资源中设置应用信息。此方法会读取R.raw.introduction中的内容, + * 并且只有当之前未添加介绍信息时,才将读取到的内容保存为一个工作笔记。 + */ + private void setAppInfoFromRawRes() { + // 获取SharedPreferences实例 + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 检查是否已经添加了介绍信息 + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + // 从资源中打开introduction文件 + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + // 读取文件内容到StringBuilder + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char[] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + // 打印错误日志,如果无法打开文件 + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + // 确保InputStream被关闭 + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // 创建一个新的工作笔记并设置其内容 + 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(); + } else { + // 打印错误日志,如果保存工作笔记失败 + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + /** + * Activity启动时调用,开始异步查询笔记列表。 + */ + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + /** + * 初始化资源,包括ListView、适配器和其他UI组件。 + */ + private void initResources() { + // 获取ContentResolver实例 + mContentResolver = this.getContentResolver(); + // 创建后台查询处理器 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 初始化ListView和相关监听器 + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + // 初始化并设置笔记列表适配器 + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + // 初始化新建笔记按钮并设置点击监听器 + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + // 初始化状态变量和触摸相关的变量 + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + // 初始化标题栏和其他状态变量 + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + + + /** + * 用于处理列表的多选择模式和菜单点击事件的回调类。 + */ + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; // 下拉菜单 + private ActionMode mActionMode; // 动作模式 + private MenuItem mMoveMenu; // 移动菜单项 + + /** + * 创建动作模式时的回调方法。 + * + * @param mode 动作模式实例。 + * @param menu 菜单实例。 + * @return 如果成功创建动作模式,返回true;否则返回false。 + */ + 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(); + // 更新下拉菜单标题 + 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); + } + } + } + + /** + * 准备动作模式时的回调方法。 + * + * @param mode 动作模式实例。 + * @param menu 菜单实例。 + * @return 返回false,表示未进行任何操作。 + */ + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + /** + * 点击动作模式中的菜单项时的回调方法。 + * + * @param mode 动作模式实例。 + * @param item 被点击的菜单项。 + * @return 返回false,表示未进行任何操作。 + */ + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + /** + * 销毁动作模式时的回调方法。 + * + * @param mode 动作模式实例。 + */ + public void onDestroyActionMode(ActionMode mode) { + // 还原列表选择模式和设置 + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + /** + * 处理列表项选择状态变化的回调方法。 + * + * @param mode 动作模式实例。 + * @param position 列表中被改变选择状态的项的位置。 + * @param id 项的ID。 + * @param checked 项的新选择状态。 + */ + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + // 更新列表项的选择状态并更新菜单 + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + /** + * 处理菜单项点击事件的回调方法。 + * + * @param item 被点击的菜单项。 + * @return 如果已处理点击事件,返回true;否则返回false。 + */ + 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; + } + + // 根据菜单项ID执行相应操作 + switch (item.getItemId()) { + case R.id.delete: + // 显示删除确认对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + // 启动查询目标文件夹的操作 + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } + } + + + /** + * 为“新建笔记”按钮添加触摸监听器的内部类,实现点击和拖动事件的处理。 + */ + private class NewNoteOnTouchListener implements OnTouchListener { + + /** + * 处理触摸事件。 + * + * @param v 触摸的视图。 + * @param event 触摸事件。 + * @return 如果事件被处理则返回true,否则返回false。 + */ + public boolean onTouch(View v, MotionEvent event) { + // 根据触摸事件的动作进行不同的处理 + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // 获取屏幕高度和“新建笔记”视图的高度 + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + // 如果当前状态为子文件夹编辑状态,需减去标题栏的高度 + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + // 当点击到“新建笔记”按钮透明部分时,将事件分发给背后的列表视图 + // 这里使用了一种硬编码的方式处理透明部分的点击,依赖于当前的背景公式 + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + 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; + } + } + // 如果事件未被分发,则返回false + return false; + } + + } + + ; + + + /** + * 异步查询笔记列表。 + * 根据当前文件夹ID选择不同的查询条件,启动一个后台查询处理该查询。 + */ + private void startAsyncNotesListQuery() { + // 根据当前文件夹ID选择查询条件 + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + // 启动查询,排序方式为类型降序,修改日期降序 + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[]{ + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + + /** + * 处理后台查询的类。 + * 继承自AsyncQueryHandler,用于处理异步查询完成后的操作。 + */ + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + /** + * 查询完成时的处理逻辑。 + * 根据查询标记的不同,执行不同的操作,如更新笔记列表或显示文件夹列表。 + * + * @param token 查询标记,用于区分不同的查询。 + * @param cookie 查询时传入的附加对象。 + * @param cursor 查询结果的游标。 + */ + @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; + } + } + } + + /** + * 显示文件夹列表的菜单。 + * 使用查询结果构建一个对话框,让用户选择一个文件夹。 + * + * @param cursor 查询结果的游标,包含文件夹信息。 + */ + private void showFolderListMenu(Cursor cursor) { + // 构建文件夹列表选择的对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + /** + * 用户选择文件夹时的处理逻辑。 + * 将选中的笔记移动到用户选择的文件夹中,并给出反馈。 + * + * @param dialog 对话框实例。 + * @param which 用户选择的项的索引。 + */ + public void onClick(DialogInterface dialog, int which) { + // 批量移动选中的笔记到目标文件夹 + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + // 显示移动操作的反馈信息 + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + // 结束当前的操作模式 + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + + /** + * 创建新的笔记。 + * 启动一个活动用于编辑新笔记或编辑现有笔记。 + */ + private void createNewNote() { + // 构建意图并指定动作为插入或编辑,以及初始文件夹ID + 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); + } + + + /** + * 批量删除笔记的函数。根据当前是否处于同步模式,采取不同的删除策略:如果不处于同步模式,则直接删除笔记;如果处于同步模式,则将笔记移动到回收站文件夹。 + * 执行删除操作后,会更新相应的widgets。 + */ + private void batchDelete() { + new AsyncTask>() { + // 在后台执行任务,获取选中的widgets并执行删除操作 + protected HashSet doInBackground(Void... unused) { + // 获取当前选中的widgets + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // 如果当前不处于同步模式,直接删除笔记 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + // 删除成功无需额外操作 + } else { + // 删除失败,记录错误 + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果处于同步模式,将笔记移动到回收站文件夹 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + // 移动失败,记录错误 + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + + // 删除操作完成后,在UI线程执行后续操作 + @Override + protected void onPostExecute(HashSet widgets) { + // 遍历所有受影响的widgets,对有效的widgets进行更新 + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + // 更新有效的widget + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + // 结束动作模式 + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + + /** + * 删除指定的文件夹。 + * 如果是在同步模式下,文件夹会被移动到回收站,否则直接删除。 + * 同时,也会更新与该文件夹相关的所有小部件。 + * + * @param folderId 要删除的文件夹ID。 + */ + private void deleteFolder(long folderId) { + // 根据ID判断是否为根文件夹,根文件夹不能被删除 + 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()) { + // 非同步模式下直接删除文件夹 + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // 同步模式下将文件夹移动到回收站 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + + // 更新相关小部件 + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + // 有效的小部件才进行更新 + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + /** + * 打开指定的笔记节点进行编辑。 + * + * @param data 包含要打开的笔记节点信息的对象。 + */ + private void openNode(NoteItemData data) { + // 构造Intent并设置动作和额外数据,然后启动Activity + 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); + } + + /** + * 打开指定的文件夹,并加载其笔记列表。 + * 根据文件夹ID的不同,更新UI状态,包括标题和新增笔记按钮的可见性。 + * + * @param data 包含要打开的文件夹信息的对象。 + */ + private void openFolder(NoteItemData data) { + // 设置当前文件夹ID并启动异步查询 + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + + // 根据文件夹ID更新UI状态 + 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); + } + + /** + * 点击事件的处理方法。 + * 目前仅处理新建笔记按钮的点击事件。 + * + * @param v 被点击的视图对象。 + */ + public void onClick(View v) { + // 根据视图ID执行相应的操作 + switch (v.getId()) { + case R.id.btn_new_note: + createNewNote(); + break; + default: + break; + } + } + + + /** + * 显示软键盘。 + */ + private void showSoftInput() { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + /** + * 隐藏软键盘。 + * + * @param view 触发隐藏软键盘的视图。 + */ + private void hideSoftInput(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * 显示创建或修改文件夹的对话框。 + * + * @param create 如果为true,则为创建文件夹;如果为false,则为修改文件夹。 + */ + 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"); // 日志记录,长按的数据项为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); + } + + // 监听输入框文本变化,以动态启用或禁用确定按钮 + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // 空实现 + } + + 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) { + // 空实现 + } + }); + } + + + /** + * 当用户按下返回键时调用的方法,根据当前状态执行不同的操作。 + * 在子文件夹状态下,返回根文件夹并显示笔记列表; + * 在通话记录文件夹状态下,也返回根文件夹但显示添加新笔记按钮; + * 在笔记列表状态下,执行父类的onBackPressed方法,通常是退出或返回上一级。 + */ + @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; + } + } + + /** + * 更新小部件显示。 + * 根据传入的小部件类型,设置对应的Provider并发送更新广播。 + * + * @param appWidgetId 小部件ID + * @param appWidgetType 小部件类型 + */ + private void updateWidget(int appWidgetId, int appWidgetType) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据小部件类型设置Provider + 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); + } + } + }; + + /** + * 上下文菜单关闭时的回调方法。 + * 在列表视图中取消上下文菜单的监听器。 + * + * @param menu 被关闭的菜单对象 + */ + @Override + public void onContextMenuClosed(Menu menu) { + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + super.onContextMenuClosed(menu); + } + + + /** + * 当上下文菜单中的项目被选择时调用。 + * + * @param item 被选择的菜单项。 + * @return 如果事件已成功处理,则返回true;否则如果事件未处理,则返回false。 + */ + @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; + } + + /** + * 准备选项菜单。 + * + * @param menu 菜单对象。 + * @return 总是返回true。 + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); // 清除之前的菜单项 + // 根据当前状态加载不同的菜单布局 + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // 设置同步或取消同步菜单项的标题 + 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; + } + + /** + * 处理选项菜单项的选择。 + * + * @param item 被选择的菜单项。 + * @return 如果事件已成功处理,则返回true;否则如果事件未处理,则返回false。 + */ + @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; + } + + /** + * 处理搜索请求。 + * + * @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) { + showExportFailedDialog(NotesListActivity.this.getString(R.string.failed_sdcard_export), + NotesListActivity.this.getString(R.string.error_sdcard_unmounted)); + } else if (result == BackupUtils.STATE_SUCCESS) { + showExportSuccessDialog(NotesListActivity.this.getString(R.string.success_sdcard_export), + backup.getExportedTextFileName(), backup.getExportedTextFileDir()); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + showExportFailedDialog(NotesListActivity.this.getString(R.string.failed_sdcard_export), + NotesListActivity.this.getString(R.string.error_sdcard_export)); + } + } + + }.execute(); + } + + /** + * 检查当前是否为同步模式。 + * + * @return 如果已配置同步账户名则返回true,否则返回false。 + */ + 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"); + } + + /** + * 长按列表项时的处理。 + * 根据不同的项类型启动选择模式或显示上下文菜单。 + * + * @return 总是返回false。 + */ + 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; + } + + /** + * 显示导出失败的对话框。 + * + * @param title 对话框标题 + * @param message 对话框消息内容 + */ + private void showExportFailedDialog(String title, String message) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(title); + builder.setMessage(message); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + + /** + * 显示导出成功的对话框。 + * + * @param title 对话框标题 + * @param fileName 导出文件的名称 + * @param fileDir 导出文件的目录 + */ + private void showExportSuccessDialog(String title, String fileName, String fileDir) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(title); + builder.setMessage(NotesListActivity.this.getString(R.string.format_exported_file_location, fileName, fileDir)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + +} diff --git a/src/ui/NotesListAdapter.java b/src/ui/NotesListAdapter.java new file mode 100644 index 0000000..057de6a --- /dev/null +++ b/src/ui/NotesListAdapter.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.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; + + +/** + * 用于管理笔记列表的适配器,继承自CursorAdapter。 + */ +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + // 用于存储选中项的索引和状态 + private HashMap mSelectedIndex; + private int mNotesCount; // 笔记总数 + private boolean mChoiceMode; // 选择模式标志 + + /** + * AppWidget属性容器,用于存储与小部件相关的数据。 + */ + 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的HashSet + */ + 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, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + + return itemSet; + } + + /** + * 获取所有选中小部件的属性集合。 + * + * @return 选中小部件属性的HashSet + */ + 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, "Invalid cursor"); + 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, "Invalid cursor"); + return; + } + } + } +} + diff --git a/src/ui/NotesListItem.java b/src/ui/NotesListItem.java new file mode 100644 index 0000000..1e53b26 --- /dev/null +++ b/src/ui/NotesListItem.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + + +/* + * 该类表示一个笔记列表项,继承自LinearLayout,并包含了显示笔记各种信息的组件。 + * 它用于在UI中展示一个笔记或文件夹的条目。 + */ + +public class NotesListItem extends LinearLayout { + private ImageView mAlert; // 用于显示提醒图标 + private TextView mTitle; // 显示笔记标题 + private TextView mTime; // 显示修改时间 + private TextView mCallName; // 在通话记录笔记中显示通话名称 + private NoteItemData mItemData; // 绑定的笔记数据 + private CheckBox mCheckBox; // 选择框,用于多选模式 + + /* + * 构造函数,初始化视图组件。 + */ + 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); + } + + /* + * 绑定数据到视图,根据数据设置视图状态。 + * + * @param context 上下文 + * @param data 要绑定的笔记数据 + * @param choiceMode 是否为选择模式 + * @param checked 是否选中 + */ + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 根据是否为选择模式和笔记类型,控制复选框的可见性和选中状态 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + mCheckBox.setVisibility(View.GONE); + } + + mItemData = data; + // 根据笔记类型和状态,设置标题、提醒图标和背景 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 通话记录文件夹 + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 通话记录笔记 + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } else { + // 其他类型的笔记或文件夹 + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + if (data.getType() == Notes.TYPE_FOLDER) { + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } else { + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + // 设置时间显示 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 设置背景资源 + setBackground(data); + } + + /* + * 根据笔记数据设置列表项的背景资源。 + */ + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + if (data.getType() == Notes.TYPE_NOTE) { + // 根据笔记的状态设置不同的背景资源 + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + // 文件夹背景资源 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + /* + * 获取绑定的笔记数据。 + * + * @return 绑定的NoteItemData对象 + */ + public NoteItemData getItemData() { + return mItemData; + } +} + diff --git a/src/ui/NotesPreferenceActivity.java b/src/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..76640bc --- /dev/null +++ b/src/ui/NotesPreferenceActivity.java @@ -0,0 +1,481 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +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; + + +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"; // 权限过滤键 + + // 类成员变量定义部分:主要用于账户同步和UI更新 + private PreferenceCategory mAccountCategory; // 账户分类偏好项 + private GTaskReceiver mReceiver; // 接收同步任务的广播接收器 + private Account[] mOriAccounts; // 原始账户数组 + private boolean mHasAddedAccount; // 标记是否已添加新账户 + + /** + * 当设置Activity创建时调用。 + * 主要进行界面初始化和设置账户同步。 + * + * @param icicle 保存Activity状态的Bundle,用于恢复状态。 + */ + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // 设置返回按钮 + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 从XML加载偏好设置 + addPreferencesFromResource(R.xml.preferences); + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + registerReceiver(mReceiver, filter); // 注册广播接收器以监听同步服务 + + mOriAccounts = null; + // 添加设置头部视图 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); + } + + /** + * 当设置Activity恢复到前台时调用。 + * 主要用于检查并自动设置新添加的账户进行同步。 + */ + @Override + protected void onResume() { + super.onResume(); + + // 自动设置新添加的账户进行同步 + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); // 设置新账户进行同步 + break; + } + } + } + } + + // 刷新UI + refreshUI(); + } + + + /** + * 当Activity即将被销毁时调用,用于注销广播接收器。 + */ + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); // 注销广播接收器,避免内存泄漏 + } + super.onDestroy(); + } + + /** + * 加载账户偏好设置,展示当前同步账户信息及操作。 + */ + private void loadAccountPreference() { + mAccountCategory.removeAll(); // 清空账户分类下的所有条目 + + // 创建并配置账户偏好项 + Preference accountPref = new Preference(this); + final String defaultAccount = getSyncAccountName(this); // 获取默认同步账户名称 + accountPref.setTitle(getString(R.string.preferences_account_title)); // 设置标题 + accountPref.setSummary(getString(R.string.preferences_account_summary)); // 设置摘要 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + // 处理账户点击事件 + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // 如果尚未设置账户,则展示选择账户对话框 + showSelectAccountAlertDialog(); + } else { + // 如果已经设置账户,则展示更改账户确认对话框 + showChangeAccountConfirmAlertDialog(); + } + } else { + // 如果正在同步中,则展示无法更改账户的提示 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + 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); // 获取上次同步时间视图 + + // 根据同步状态设置按钮文本和点击事件 + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); // 设置为取消同步文本 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 设置点击事件为取消同步 + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); // 设置为立即同步文本 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); // 设置点击事件为开始同步 + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); // 只有在设置了同步账户时才使能同步按钮 + + // 根据同步状态设置上次同步时间的显示 + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); // 如果正在同步,显示进度信息 + lastSyncTimeView.setVisibility(View.VISIBLE); // 显示上次同步时间视图 + } else { + long lastSyncTime = getLastSyncTime(this); // 获取上次同步时间 + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); // 格式化并显示上次同步时间 + lastSyncTimeView.setVisibility(View.VISIBLE); // 显示上次同步时间视图 + } else { + lastSyncTimeView.setVisibility(View.GONE); // 如果未同步过,则隐藏上次同步时间视图 + } + } + } + + /** + * 刷新用户界面,加载账户偏好设置和同步按钮。 + */ + private void refreshUI() { + loadAccountPreference(); // 加载账户偏好设置 + loadSyncButton(); // 加载同步按钮 + } + + /** + * 显示选择账户的对话框。 + * 该对话框列出了已连接的Google账户,并允许用户选择一个账户用于同步。 + * 如果没有账户,对话框将提供添加账户的选项。 + */ + private void showSelectAccountAlertDialog() { + // 创建对话框构建器并设置自定义标题 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); // 移除默认的确定按钮 + + // 获取当前设备上的Google账户 + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); // 获取当前同步的账户名称 + + mOriAccounts = accounts; // 保存原始账户列表 + mHasAddedAccount = false; // 标记是否已添加新账户 + + if (accounts.length > 0) { + // 创建账户选项并设置选中项 + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; // 记录默认选中的账户 + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + // 设置单选列表,并为选中的账户执行同步操作 + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + // 添加“添加账户”选项 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + final AlertDialog dialog = dialogBuilder.show(); + // 点击“添加账户”执行添加账户操作 + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[]{ + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); + } + + /** + * 显示更改账户确认对话框。 + * 提供用户更改当前同步账户或取消更改的选择。 + */ + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 设置自定义标题,包含当前同步账户名称 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + // 创建菜单项并设置点击事件 + CharSequence[] menuItemArray = new CharSequence[]{ + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + // 选择更改账户,显示账户选择对话框 + showSelectAccountAlertDialog(); + } else if (which == 1) { + // 选择移除账户,执行移除操作并刷新UI + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + } + + /** + * 获取设备上的Google账户列表。 + * + * @return Account[] 返回设备上所有类型为“com.google”的账户数组。 + */ + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + + /** + * 设置同步账户信息。 + * 如果当前账户与传入账户不一致,则更新SharedPreferences中的账户信息,并清理本地相关的gtask信息。 + * + * @param account 需要设置的账户名 + */ + private void setSyncAccount(String account) { + // 检查当前账户是否与传入账户名一致,不一致则更新账户信息 + if (!getSyncAccountName(this).equals(account)) { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + // 如果账户名非空,则保存账户名,否则清除账户名 + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // 清理上次同步时间 + setLastSyncTime(this, 0); + + // 清理本地相关的gtask信息 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + // 显示设置成功的提示信息 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + /** + * 移除同步账户信息。 + * 清除SharedPreferences中的账户信息和上次同步时间,并清理本地相关的gtask信息。 + */ + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + // 如果存在账户信息,则移除 + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + // 如果存在上次同步时间信息,则移除 + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // 清理本地相关的gtask信息 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + /** + * 获取当前同步账户名。 + * 从SharedPreferences中获取存储的账户名,默认为空字符串。 + * + * @param context 上下文 + * @return 同步账户名 + */ + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + /** + * 设置上次同步的时间。 + * 将指定的时间保存到SharedPreferences中。 + * + * @param context 上下文 + * @param time 上次同步的时间戳 + */ + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); + } + + /** + * 获取上次同步的时间。 + * 从SharedPreferences中获取上次同步的时间戳,默认为0。 + * + * @param context 上下文 + * @return 上次同步的时间戳 + */ + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + /** + * 广播接收器类,用于接收gtask同步相关的广播消息,并据此刷新UI。 + */ + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); + // 如果广播消息表明正在同步,则更新UI显示的同步状态信息 + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } + } + + /** + * 处理选项菜单项的选择事件。 + * + * @param item 选中的菜单项 + * @return 如果事件已处理,则返回true;否则返回false。 + */ + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // 当选择返回按钮时,启动NotesListActivity并清除当前活动栈顶以上的所有活动 + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } + } +} diff --git a/src/widget/NoteWidgetProvider.java b/src/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..c746e27 --- /dev/null +++ b/src/widget/NoteWidgetProvider.java @@ -0,0 +1,159 @@ +package net.micode.notes.widget; + +// 导入必要的 Android 和应用相关的类和包 +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +// 抽象类 NoteWidgetProvider,继承自 AppWidgetProvider,用于定义笔记小部件的逻辑 +public abstract class NoteWidgetProvider extends AppWidgetProvider { + + // 定义数据库查询时所需的列名数组 + public static final String[] PROJECTION = new String[]{ + NoteColumns.ID, // 笔记的唯一 ID + NoteColumns.BG_COLOR_ID, // 笔记的背景颜色 ID + NoteColumns.SNIPPET // 笔记的摘要(内容片段) + }; + + // 定义列索引的常量,以便访问查询结果时更加清晰和方便 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + // 定义日志标签,便于在调试和记录日志时标识日志来源 + private static final String TAG = "NoteWidgetProvider"; + + // 覆写 onDeleted 方法,当小部件被用户移除时调用 + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // 创建一个 ContentValues 对象,用于更新数据库 + ContentValues values = new ContentValues(); + // 将小部件 ID 设置为无效 ID,以便从数据库中移除相关关联 + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + // 遍历被删除的小部件 ID 数组 + for (int i = 0; i < appWidgetIds.length; i++) { + // 更新数据库中与该小部件关联的记录 + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", // WHERE 子句:匹配指定小部件 ID + new String[]{String.valueOf(appWidgetIds[i])}); // WHERE 参数 + } + } + + // 私有方法:根据小部件 ID 查询对应的笔记信息 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + // 执行数据库查询,返回与指定小部件 ID 相关的笔记 + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, // 查询的列 + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", // 条件:小部件 ID 匹配且不在垃圾箱中 + new String[]{String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER)}, // 参数值 + null); // 排序方式(此处为 null,即不排序) + } + + // 受保护方法:更新小部件显示内容的公共接口 + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 默认调用私有方法,并将隐私模式设置为 false + update(context, appWidgetManager, appWidgetIds, false); + } + + // 私有方法:根据隐私模式,更新小部件显示内容 + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + // 遍历所有小部件 ID + for (int i = 0; i < appWidgetIds.length; i++) { + // 如果小部件 ID 无效,则跳过 + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 获取默认的背景颜色 ID + int bgId = ResourceParser.getDefaultBgId(context); + // 初始化笔记摘要为空字符串 + String snippet = ""; + // 创建一个 Intent,指定跳转到 NoteEditActivity + Intent intent = new Intent(context, NoteEditActivity.class); + // 设置 Intent 的标志,确保活动以单一实例模式启动 + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + // 将小部件 ID 和类型作为额外数据加入 Intent + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 查询数据库,获取与当前小部件 ID 关联的笔记信息 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + // 如果查询结果不为空且有数据 + if (c != null && c.moveToFirst()) { + // 如果查询结果超过一条记录,则记录日志并返回 + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 从查询结果中获取笔记摘要和背景颜色 ID + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + // 将笔记 ID 添加到 Intent 的额外数据中 + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + // 设置 Intent 的操作类型为查看 + intent.setAction(Intent.ACTION_VIEW); + } else { + // 如果查询结果为空,则设置默认的内容和操作类型 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 如果 Cursor 对象不为空,则关闭以释放资源 + if (c != null) { + c.close(); + } + + // 创建 RemoteViews 对象,用于更新小部件的布局 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // 设置小部件的背景图片资源 + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + // 将背景颜色 ID 添加到 Intent 的额外数据中 + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 声明 PendingIntent 对象,用于处理小部件的点击事件 + PendingIntent pendingIntent = null; + if (privacyMode) { + // 如果处于隐私模式,则显示隐私模式文本 + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + // 设置 PendingIntent,点击时跳转到 NotesListActivity + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 非隐私模式,显示笔记摘要 + rv.setTextViewText(R.id.widget_text, snippet); + // 设置 PendingIntent,点击时跳转到笔记编辑页面 + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + // 为小部件的文本区域设置点击事件 + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 更新小部件显示内容 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + // 抽象方法:获取背景资源的 ID,具体实现由子类提供 + protected abstract int getBgResourceId(int bgId); + + // 抽象方法:获取小部件布局的资源 ID,具体实现由子类提供 + protected abstract int getLayoutId(); + + // 抽象方法:获取小部件的类型,具体实现由子类提供 + protected abstract int getWidgetType(); +} diff --git a/src/widget/NoteWidgetProvider_2x.java b/src/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..9a547d2 --- /dev/null +++ b/src/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,76 @@ +/* + * 版权声明:MiCode开源社区(www.micode.net) + * + * 本代码遵循Apache 2.0开源协议 + * 如需获取完整的授权条款,请访问:http://www.apache.org/licenses/LICENSE-2.0.html + * + * 代码开始 + */ + +// 定义包名,组织代码模块化管理 +package net.micode.notes.widget; + +// 导入Android小部件相关类和应用的资源类 +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; // 导入资源文件R类,用于访问布局和其他资源 +import net.micode.notes.data.Notes; // 导入笔记相关的数据类 +import net.micode.notes.tool.ResourceParser; // 导入资源解析工具类 + +/** + * 2x版本的小部件提供者类。 + * 该类继承自 NoteWidgetProvider,专门处理 2x2 尺寸的笔记小部件。 + */ +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + /** + * 当系统需要更新小部件时,会调用此方法。 + * 此方法通过调用父类的 `update` 方法来处理小部件更新的逻辑。 + * + * @param context 上下文环境,用于获取应用程序资源和操作。 + * @param appWidgetManager 管理当前应用中所有小部件的 AppWidgetManager 实例。 + * @param appWidgetIds 当前需要更新的小部件 ID 数组。 + */ + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的 update 方法,完成小部件的更新逻辑 + super.update(context, appWidgetManager, appWidgetIds); + } + + /** + * 获取小部件布局的资源 ID。 + * 此方法返回当前小部件所使用的布局文件的资源 ID。 + * 该布局文件定义了小部件在屏幕上的外观。 + * + * @return 布局资源的 ID,对应 `res/layout/widget_2x.xml` 文件。 + */ + @Override + protected int getLayoutId() { + return R.layout.widget_2x; // 返回 2x 小部件的布局资源 ID + } + + /** + * 根据背景 ID 获取对应的背景资源 ID。 + * 小部件有不同的背景样式(如不同颜色或主题),通过背景 ID 选择对应的资源。 + * + * @param bgId 背景资源的索引 ID,表示选择哪一种背景样式。 + * @return 背景资源的 ID,用于设置小部件的背景。 + */ + @Override + protected int getBgResourceId(int bgId) { + // 使用 ResourceParser 工具类,根据背景 ID 获取 2x 小部件的背景资源 ID + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + /** + * 获取当前小部件的类型。 + * 该方法返回一个常量,用于标识当前小部件的类型。 + * 不同尺寸的小部件有不同的类型常量,例如 2x 和 4x 小部件。 + * + * @return 小部件类型的常量值,表示当前为 2x 尺寸的小部件。 + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; // 返回表示 2x 小部件类型的常量 + } +} diff --git a/src/widget/NoteWidgetProvider_4x.java b/src/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..29c55e0 --- /dev/null +++ b/src/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,74 @@ +/* + * 版权声明:MiCode开源社区(www.micode.net) + * + * 本代码遵循Apache 2.0开源协议 + * 详细授权信息请访问:http://www.apache.org/licenses/LICENSE-2.0 + */ + +// 定义代码所在的包,用于组织代码模块化 +package net.micode.notes.widget; + +// 导入小部件和应用程序相关的类 +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; // 导入应用资源文件 R,访问布局和其他资源 +import net.micode.notes.data.Notes; // 导入笔记相关的常量和数据类 +import net.micode.notes.tool.ResourceParser; // 导入工具类,用于解析小部件资源 + +/** + * 4x大小的便签小部件提供者类,继承自 NoteWidgetProvider。 + * 专门用于处理 4x 大小的便签小部件的更新和相关操作。 + */ +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + /** + * 当系统请求更新小部件时,会调用此方法。 + * 该方法通过调用父类的 `update` 方法来处理小部件的更新逻辑。 + * + * @param context 上下文环境,提供应用程序的全局信息和操作能力。 + * @param appWidgetManager 小部件管理器,用于管理当前应用中所有的小部件。 + * @param appWidgetIds 需要更新的小部件 ID 数组,包含多个小部件的唯一标识符。 + */ + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的 update 方法,完成通用的更新逻辑 + super.update(context, appWidgetManager, appWidgetIds); + } + + /** + * 获取小部件的布局资源 ID。 + * 布局资源定义了小部件的界面结构,例如显示的文本和背景图片等。 + * + * @return 返回 4x 小部件的布局资源 ID,对应 `res/layout/widget_4x.xml` 文件。 + */ + @Override + protected int getLayoutId() { + return R.layout.widget_4x; // 指定用于 4x 小部件的布局资源 + } + + /** + * 根据背景 ID 获取对应的背景资源 ID。 + * 小部件可以有不同的背景样式,例如不同的颜色或主题,通过背景 ID 来选择。 + * + * @param bgId 背景的索引 ID,表示不同的背景样式。 + * @return 返回与背景 ID 对应的背景资源 ID,用于设置小部件的背景。 + */ + @Override + protected int getBgResourceId(int bgId) { + // 使用 ResourceParser 工具类,根据背景 ID 获取 4x 小部件的背景资源 ID + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + /** + * 获取当前小部件的类型标识。 + * 该方法返回一个整型常量,用于标识当前小部件的类型。 + * 在应用中可以通过该标识区分不同的小部件类型,例如 4x 和 2x 小部件。 + * + * @return 返回小部件类型的整型标识,表示这是一个 4x 大小的小部件。 + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; // 返回 4x 小部件类型的常量 + } +} From 57367617681c1397706f2cb13463b607d9bb76b6 Mon Sep 17 00:00:00 2001 From: zpz1349878361 <1349878361@qq.com> Date: Wed, 8 Jan 2025 21:06:39 +0800 Subject: [PATCH 2/5] xin --- ...签开源代码全文泛读报告 .docx | Bin 525875 -> 560203 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签开源代码全文泛读报告 .docx b/doc/小米便签开源代码全文泛读报告 .docx index f063739538f6938f927a3646a814d84ca6f178f3..9de2d35ac1cc27a27d7ebf1454688b4464058488 100644 GIT binary patch delta 76789 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
UQB4tIeooE>Z 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 IBG8rp16itdF#rGn delta 43937 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 From d2ea83f3f42aa3bf02a7e7afa11ce1556bde47e2 Mon Sep 17 00:00:00 2001 From: zpz1349878361 <1349878361@qq.com> Date: Wed, 8 Jan 2025 21:15:24 +0800 Subject: [PATCH 3/5] xin --- ...签开源代码全文泛读报告 .docx | Bin 560203 -> 560279 bytes src/BackupUtils.java | 279 ------------ src/DataUtils.java | 406 ------------------ src/GTaskStringUtils.java | 64 --- src/ResourceParser.java | 212 --------- 5 files changed, 961 deletions(-) delete mode 100644 src/BackupUtils.java delete mode 100644 src/DataUtils.java delete mode 100644 src/GTaskStringUtils.java delete mode 100644 src/ResourceParser.java diff --git a/doc/小米便签开源代码全文泛读报告 .docx b/doc/小米便签开源代码全文泛读报告 .docx index 9de2d35ac1cc27a27d7ebf1454688b4464058488..6370842d49f4556fc379d6c260e32b056e290451 100644 GIT binary patch delta 52516 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 delta 52329 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 U%~%fUc11{u1&Tb2RKS_~2T@l&(EtDd diff --git a/src/BackupUtils.java b/src/BackupUtils.java deleted file mode 100644 index 821cbd6..0000000 --- a/src/BackupUtils.java +++ /dev/null @@ -1,279 +0,0 @@ - -/* - * BackupUtils 工具类 - * 提供了笔记备份到文本文件的功能。 - */ - -package net.micode.notes.tool; - -// 引入需要用到的 Android 和 Java 标准库类 -import android.content.Context; -import android.database.Cursor; -import android.os.Environment; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.util.Log; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintStream; - -/** - * BackupUtils 是一个工具类,用于实现笔记的备份功能。 - */ -public class BackupUtils { - - // 日志标志,用于记录类中的日志 - private static final String TAG = "BackupUtils"; - - // 单例模式:存储唯一实例 - private static BackupUtils sInstance; - - /** - * 获取 BackupUtils 类的单例实例。 - * - * @param context 上下文对象,用于初始化内部组件。 - * @return 返回 BackupUtils 实例。 - */ - public static synchronized BackupUtils getInstance(Context context) { - if (sInstance == null) { // 如果实例不存在,则创建新实例 - sInstance = new BackupUtils(context); - } - return sInstance; // 返回单例实例 - } - - /** - * 定义备份操作的状态常量 - */ - public static final int STATE_SD_CARD_UNMOUONTED = 0; // SD卡未挂载 - public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // 备份文件不存在 - public static final int STATE_DATA_DESTROIED = 2; // 数据已损坏 - public static final int STATE_SYSTEM_ERROR = 3; // 系统错误 - public static final int STATE_SUCCESS = 4; // 备份成功 - - // 用于文本导出的工具类 - private TextExport mTextExport; - - /** - * 私有化构造函数,防止直接实例化。 - * - * @param context 上下文对象,用于初始化导出工具类。 - */ - private BackupUtils(Context context) { - mTextExport = new TextExport(context); // 初始化文本导出工具 - } - - /** - * 检查 SD 卡是否可用。 - * - * @return 如果 SD 卡已挂载,则返回 true,否则返回 false。 - */ - private static boolean externalStorageAvailable() { - return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); - } - - /** - * 导出笔记到文本文件。 - * - * @return 返回导出操作的状态码。 - */ - public int exportToText() { - return mTextExport.exportToText(); // 调用 TextExport 类中的导出方法 - } - - /** - * 获取导出文本的文件名。 - * - * @return 返回文件名字符串。 - */ - public String getExportedTextFileName() { - return mTextExport.mFileName; // 返回文件名 - } - - /** - * 获取导出文件的目录。 - * - * @return 返回目录路径字符串。 - */ - public String getExportedTextFileDir() { - return mTextExport.mFileDirectory; // 返回文件目录路径 - } - - /** - * 内部类,用于执行文本导出功能。 - */ - private static class TextExport { - - // 定义查询笔记的列名 - private static final String[] NOTE_PROJECTION = { - NoteColumns.ID, // 笔记 ID - NoteColumns.MODIFIED_DATE, // 修改时间 - NoteColumns.SNIPPET, // 摘要 - NoteColumns.TYPE // 类型 - }; - - // 定义笔记列的索引 - private static final int NOTE_COLUMN_ID = 0; // ID 列索引 - private static final int NOTE_COLUMN_MODIFIED_DATE = 1; // 修改时间列索引 - private static final int NOTE_COLUMN_SNIPPET = 2; // 摘要列索引 - - // 定义查询数据表的列 - private static final String[] DATA_PROJECTION = { - DataColumns.CONTENT, // 内容 - DataColumns.MIME_TYPE, // MIME 类型 - DataColumns.DATA1, // 数据 1 - DataColumns.DATA2, // 数据 2 - DataColumns.DATA3, // 数据 3 - DataColumns.DATA4, // 数据 4 - }; - - // 定义数据列的索引 - private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引 - private static final int DATA_COLUMN_MIME_TYPE = 1; // MIME 类型列索引 - - // 上下文对象,用于访问系统资源 - private Context mContext; - private String mFileName; // 文件名 - private String mFileDirectory; // 文件目录 - - // 初始化文本导出格式 - private final String[] TEXT_FORMAT; - - public TextExport(Context context) { - TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); // 导出格式 - mContext = context; - mFileName = ""; - mFileDirectory = ""; - } - - /** - * 根据文件夹 ID 将笔记导出为文本。 - * - * @param folderId 文件夹 ID。 - * @param ps 打印流,用于写入文本内容。 - */ - private void exportFolderToText(String folderId, PrintStream ps) { - Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, - NoteColumns.PARENT_ID + "=?", new String[]{folderId}, null); - - if (notesCursor != null) { - if (notesCursor.moveToFirst()) { - do { - // 输出笔记的修改时间 - ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( - mContext.getString(R.string.format_datetime_mdhm), - notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); - // 导出笔记内容 - String noteId = notesCursor.getString(NOTE_COLUMN_ID); - exportNoteToText(noteId, ps); - } while (notesCursor.moveToNext()); - } - notesCursor.close(); - } - } - - /** - * 导出指定笔记 ID 的内容到文本。 - * - * @param noteId 笔记 ID。 - * @param ps 打印流。 - */ - private void exportNoteToText(String noteId, PrintStream ps) { - Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, - DataColumns.NOTE_ID + "=?", new String[]{noteId}, null); - - if (dataCursor != null) { - if (dataCursor.moveToFirst()) { - do { - String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); - if (DataConstants.CALL_NOTE.equals(mimeType)) { - // 导出通话笔记 - } else if (DataConstants.NOTE.equals(mimeType)) { - // 导出普通笔记 - } - } while (dataCursor.moveToNext()); - } - dataCursor.close(); - } - } - - /** - * 获取指向文件的打印流,用于导出笔记内容。 - * - * @return 返回指向生成的文本文件的打印流对象,如果失败则返回 null。 - */ - private PrintStream getExportToTextPrintStream() { - // 调用辅助方法,生成导出文件 - File file = generateFileMountedOnSDcard(mContext, R.string.file_path, R.string.file_name_txt_format); - if (file == null) { // 如果文件生成失败 - Log.e(TAG, "create file to exported failed"); // 输出错误日志 - return null; // 返回 null,表示打印流创建失败 - } - // 更新文件名和目录信息 - mFileName = file.getName(); // 设置文件名 - mFileDirectory = mContext.getString(R.string.file_path); // 设置文件目录 - - PrintStream ps = null; // 初始化打印流为 null - try { - // 创建文件输出流并包装为打印流 - FileOutputStream fos = new FileOutputStream(file); - ps = new PrintStream(fos); - } catch (FileNotFoundException e) { // 捕获文件未找到异常 - e.printStackTrace(); - return null; // 返回 null,表示打印流创建失败 - } catch (NullPointerException e) { // 捕获空指针异常 - e.printStackTrace(); - return null; // 返回 null - } - return ps; // 返回成功创建的打印流对象 - } - - /** - * 在 SD 卡上生成用于存储导出数据的文件。 - * - * @param context 上下文对象,用于访问应用资源。 - * @param filePathResId 文件路径的资源 ID。 - * @param fileNameFormatResId 文件名格式的资源 ID,用于生成包含日期的文件名。 - * @return 返回生成的文件对象,如果失败则返回 null。 - */ - private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { - StringBuilder sb = new StringBuilder(); // 使用 StringBuilder 拼接路径和文件名 - - // 获取 SD 卡的根目录并拼接路径 - sb.append(Environment.getExternalStorageDirectory()); - sb.append(context.getString(filePathResId)); // 从资源中获取路径字符串 - - // 创建文件目录对象 - File filedir = new File(sb.toString()); - // 生成文件名并拼接到路径中,文件名包含日期信息 - sb.append(context.getString(fileNameFormatResId, - DateFormat.format(context.getString(R.string.format_date_ymd), System.currentTimeMillis()))); - File file = new File(sb.toString()); // 创建文件对象 - - try { - if (!filedir.exists()) { // 如果目录不存在 - filedir.mkdir(); // 创建目录 - } - if (!file.exists()) { // 如果文件不存在 - file.createNewFile(); // 创建文件 - } - return file; // 返回成功生成的文件对象 - } catch (SecurityException e) { // 捕获安全异常 - e.printStackTrace(); - } catch (IOException e) { // 捕获 I/O 异常 - e.printStackTrace(); - } - return null; // 如果发生异常,返回 null - } - } -} - - diff --git a/src/DataUtils.java b/src/DataUtils.java deleted file mode 100644 index 977ad28..0000000 --- a/src/DataUtils.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * Licensed under the Apache License, Version 2.0 - * 该文件定义了用于管理和操作笔记数据的实用工具类 DataUtils。 - */ - -package net.micode.notes.tool; // 声明该类所属的包路径 - -// 导入需要的类,用于操作内容提供器、数据库、日志和其他功能 -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.os.RemoteException; -import android.util.Log; - -import net.micode.notes.data.Notes; // 导入笔记数据定义 -import net.micode.notes.data.Notes.CallNote; // 导入与通话笔记相关的定义 -import net.micode.notes.data.Notes.NoteColumns; // 导入与笔记列相关的定义 -import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; // 导入笔记小部件属性定义 - -import java.util.ArrayList; // 用于创建动态数组 -import java.util.HashSet; // 用于存储无重复的集合 - -// 定义 DataUtils 工具类 -public class DataUtils { - public static final String TAG = "DataUtils"; // 定义日志标记常量 - - /** - * 批量删除笔记 - * - * @param resolver 内容解析器 - * @param ids 要删除的笔记ID集合 - * @return 如果删除成功或集合为空或为null,则返回true,否则返回false - */ - public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { - if (ids == null) { // 如果传入的ID集合为null - Log.d(TAG, "the ids is null"); // 输出调试日志 - return true; // 返回true,表示没有需要删除的内容 - } - if (ids.size() == 0) { // 如果ID集合为空 - Log.d(TAG, "no id is in the hashset"); // 输出调试日志 - return true; // 返回true - } - - // 创建一个操作列表,用于批量删除操作 - ArrayList operationList = new ArrayList(); - for (long id : ids) { // 遍历需要删除的ID集合 - if (id == Notes.ID_ROOT_FOLDER) { // 如果ID是系统根文件夹 - Log.e(TAG, "Don't delete system folder root"); // 输出错误日志 - continue; // 跳过此ID - } - // 创建删除操作 - ContentProviderOperation.Builder builder = ContentProviderOperation - .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); - operationList.add(builder.build()); // 将删除操作添加到列表 - } - try { - // 批量执行删除操作 - ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); - if (results == null || results.length == 0 || results[0] == null) { // 检查删除结果 - Log.d(TAG, "delete notes failed, ids:" + ids.toString()); // 输出调试日志 - return false; // 删除失败,返回false - } - return true; // 删除成功,返回true - } catch (RemoteException e) { // 捕获远程异常 - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 - } catch (OperationApplicationException e) { // 捕获操作应用异常 - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 - } - return false; // 出现异常时,返回false - } - - /** - * 将笔记移动到指定文件夹 - * - * @param resolver 内容解析器 - * @param id 笔记ID - * @param srcFolderId 原始文件夹ID - * @param desFolderId 目标文件夹ID - */ - public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { - ContentValues values = new ContentValues(); // 创建一个ContentValues对象 - values.put(NoteColumns.PARENT_ID, desFolderId); // 设置目标文件夹ID - values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 设置原始文件夹ID - values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改 - // 更新指定笔记的文件夹信息 - resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); - } - - /** - * 批量将笔记移动到指定文件夹 - * - * @param resolver 内容解析器 - * @param ids 要移动的笔记ID集合 - * @param folderId 目标文件夹ID - * @return 如果移动成功或集合为空或为null,则返回true,否则返回false - */ - public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { - if (ids == null) { // 如果ID集合为null - Log.d(TAG, "the ids is null"); // 输出调试日志 - return true; // 返回true - } - - // 构建更新操作的列表 - ArrayList operationList = new ArrayList(); - for (long id : ids) { // 遍历ID集合 - // 创建更新操作 - ContentProviderOperation.Builder builder = ContentProviderOperation - .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); - builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置目标文件夹ID - builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改 - operationList.add(builder.build()); // 将操作添加到列表 - } - - try { - // 批量执行更新操作 - ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); - if (results == null || results.length == 0 || results[0] == null) { // 检查操作结果 - Log.d(TAG, "move notes failed, ids:" + ids.toString()); // 输出调试日志 - return false; // 操作失败 - } - return true; // 操作成功 - } catch (RemoteException e) { // 捕获远程异常 - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 - } catch (OperationApplicationException e) { // 捕获操作应用异常 - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); // 输出错误日志 - } - return false; // 出现异常时,返回false - } - - /** - * 获取除系统文件夹外的所有用户文件夹数量 - * - * @param resolver 内容解析器 - * @return 用户文件夹数量 - */ - public static int getUserFolderCount(ContentResolver resolver) { - // 查询数据库获取用户文件夹数量,排除系统垃圾箱文件夹 - Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, - new String[]{"COUNT(*)"}, // 查询列:计数 - NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", // 条件:文件夹类型且不是垃圾箱 - new String[]{String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, // 条件值 - null); // 排序:无 - - int count = 0; // 初始化文件夹计数 - if (cursor != null) { // 如果查询结果不为空 - if (cursor.moveToFirst()) { // 移动到第一条记录 - try { - count = cursor.getInt(0); // 获取计数值 - } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 - Log.e(TAG, "get folder count failed:" + e.toString()); // 输出错误日志 - } finally { - cursor.close(); // 关闭游标 - } - } - } - return count; // 返回文件夹数量 - } -} - /** - * 检查指定类型的笔记在数据库中是否可见 - * - * @param resolver 内容解析器 - * @param noteId 笔记ID - * @param type 笔记类型 - * @return 如果可见,则返回true,否则返回false - */ - public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { - // 查询数据库,检查指定类型的笔记是否存在且不在垃圾箱中 - Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 查询的URI - null, // 查询的列,null表示查询所有列 - NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, // 条件 - new String[]{String.valueOf(type)}, // 条件值 - null); // 排序:无 - - boolean exist = false; // 初始化结果为false - if (cursor != null) { // 如果查询结果不为空 - if (cursor.getCount() > 0) { // 如果查询结果有记录 - exist = true; // 设置结果为true - } - cursor.close(); // 关闭游标 - } - return exist; // 返回结果 - } - - /** - * 检查指定的笔记ID在数据库中是否存在 - * - * @param resolver 内容解析器 - * @param noteId 笔记ID - * @return 如果存在,则返回true,否则返回false - */ - public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { - // 查询数据库,检查笔记是否存在 - Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 查询的URI - null, // 查询所有列 - null, // 无条件 - null, // 无条件值 - null); // 无排序 - - boolean exist = false; // 初始化结果为false - if (cursor != null) { // 如果查询结果不为空 - if (cursor.getCount() > 0) { // 如果查询结果有记录 - exist = true; // 设置结果为true - } - cursor.close(); // 关闭游标 - } - return exist; // 返回结果 - } - - /** - * 检查指定的数据ID在数据库中是否存在 - * - * @param resolver 内容解析器 - * @param dataId 数据ID - * @return 如果存在,则返回true,否则返回false - */ - public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { - // 查询数据库,检查数据是否存在 - Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), // 查询的URI - null, // 查询所有列 - null, // 无条件 - null, // 无条件值 - null); // 无排序 - - boolean exist = false; // 初始化结果为false - if (cursor != null) { // 如果查询结果不为空 - if (cursor.getCount() > 0) { // 如果查询结果有记录 - exist = true; // 设置结果为true - } - cursor.close(); // 关闭游标 - } - return exist; // 返回结果 - } - - /** - * 检查文件夹名称是否在数据库中已存在(不包括系统文件夹) - * - * @param resolver 内容解析器 - * @param name 文件夹名称 - * @return 如果已存在,则返回true,否则返回false - */ - public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { - // 查询数据库,检查文件夹名称是否存在且不在垃圾箱中 - Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, // 查询的URI - null, // 查询所有列 - NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + // 条件:类型为文件夹 - " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + // 且不在垃圾箱 - " AND " + NoteColumns.SNIPPET + "=?", // 且名称匹配 - new String[]{name}, // 条件值 - null); // 无排序 - - boolean exist = false; // 初始化结果为false - if (cursor != null) { // 如果查询结果不为空 - if (cursor.getCount() > 0) { // 如果查询结果有记录 - exist = true; // 设置结果为true - } - cursor.close(); // 关闭游标 - } - return exist; // 返回结果 - } - - /** - * 获取指定文件夹中的笔记小部件信息集合 - * - * @param resolver 内容解析器 - * @param folderId 文件夹ID - * @return 笔记小部件信息集合 - */ - public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { - // 查询数据库,获取文件夹中的小部件信息 - Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, // 查询的URI - new String[]{NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE}, // 查询的列 - NoteColumns.PARENT_ID + "=?", // 条件:父ID为指定文件夹 - new String[]{String.valueOf(folderId)}, // 条件值 - null); // 无排序 - - HashSet set = null; // 初始化小部件集合为null - if (c != null) { // 如果查询结果不为空 - if (c.moveToFirst()) { // 移动到第一条记录 - set = new HashSet(); // 初始化小部件集合 - do { // 遍历查询结果 - try { - AppWidgetAttribute widget = new AppWidgetAttribute(); // 创建小部件属性对象 - widget.widgetId = c.getInt(0); // 获取小部件ID - widget.widgetType = c.getInt(1); // 获取小部件类型 - set.add(widget); // 将小部件添加到集合 - } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 - Log.e(TAG, e.toString()); // 输出错误日志 - } - } while (c.moveToNext()); // 移动到下一条记录 - } - c.close(); // 关闭游标 - } - return set; // 返回小部件集合 - } - - - /** - * 通过笔记ID获取关联的通话号码 - * - * @param resolver 内容解析器 - * @param noteId 笔记ID - * @return 通话号码,如果未找到则返回空字符串 - */ - public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { - // 查询数据库,获取与笔记ID关联的通话号码 - Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, // 查询的URI - new String[]{CallNote.PHONE_NUMBER}, // 查询的列:电话号码 - CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", // 条件:匹配笔记ID和MIME类型 - new String[]{String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE}, // 条件值 - null); // 无排序 - - if (cursor != null && cursor.moveToFirst()) { // 如果查询结果不为空且有记录 - try { - return cursor.getString(0); // 返回查询结果中的电话号码 - } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 - Log.e(TAG, "Get call number fails " + e.toString()); // 输出错误日志 - } finally { - cursor.close(); // 关闭游标 - } - } - return ""; // 未找到记录时返回空字符串 - } - - /** - * 根据电话号码和通话日期获取对应的笔记ID - * - * @param resolver 内容解析器 - * @param phoneNumber 电话号码 - * @param callDate 通话日期 - * @return 笔记ID,未找到则返回0 - */ - public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { - // 查询数据库,根据电话号码和通话日期获取笔记ID - Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, // 查询的URI - new String[]{CallNote.NOTE_ID}, // 查询的列:笔记ID - CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" - + CallNote.PHONE_NUMBER + ",?)", // 条件:匹配通话日期、MIME类型和电话号码 - new String[]{String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber}, // 条件值 - null); // 无排序 - - if (cursor != null) { // 如果查询结果不为空 - if (cursor.moveToFirst()) { // 如果有记录 - try { - return cursor.getLong(0); // 返回笔记ID - } catch (IndexOutOfBoundsException e) { // 捕获索引越界异常 - Log.e(TAG, "Get call note id fails " + e.toString()); // 输出错误日志 - } - } - cursor.close(); // 关闭游标 - } - return 0; // 未找到记录时返回0 - } - - /** - * 根据笔记ID从数据库中获取笔记的摘要 - * - * @param resolver 内容解析器 - * @param noteId 笔记的ID - * @return 笔记的摘要字符串。如果找不到对应的笔记,将抛出IllegalArgumentException。 - */ - public static String getSnippetById(ContentResolver resolver, long noteId) { - // 查询数据库,获取指定ID笔记的摘要 - Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, // 查询的URI - new String[]{NoteColumns.SNIPPET}, // 查询的列:摘要 - NoteColumns.ID + "=?", // 条件:匹配笔记ID - new String[]{String.valueOf(noteId)}, // 条件值 - null); // 无排序 - - if (cursor != null) { // 如果查询结果不为空 - String snippet = ""; // 初始化摘要为空字符串 - if (cursor.moveToFirst()) { // 如果有记录 - snippet = cursor.getString(0); // 获取摘要字符串 - } - cursor.close(); // 关闭游标 - return snippet; // 返回摘要 - } - // 如果未找到记录,抛出异常 - throw new IllegalArgumentException("Note is not found with id: " + noteId); - } - - /** - * 格式化摘要字符串 - * 主要用于去除字符串两端的空白字符,以及截取至第一个换行符之前的内容。 - * - * @param snippet 需要格式化的摘要字符串 - * @return 格式化后的摘要字符串 - */ - public static String getFormattedSnippet(String snippet) { - // 如果摘要字符串不为空,进行格式化处理 - if (snippet != null) { - snippet = snippet.trim(); // 去除两端的空白字符 - int index = snippet.indexOf('\n'); // 查找第一个换行符的位置 - if (index != -1) { // 如果存在换行符 - snippet = snippet.substring(0, index); // 截取至第一个换行符之前的内容 - } - } - return snippet; // 返回格式化后的摘要字符串 - } -} - diff --git a/src/GTaskStringUtils.java b/src/GTaskStringUtils.java deleted file mode 100644 index aeaa30d..0000000 --- a/src/GTaskStringUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * GTaskStringUtils 类定义 - * 该类提供了一系列与 GTask 相关的字符串常量,用于在操作 GTask 数据时标识各种 JSON 属性。 - */ -package net.micode.notes.tool; // 定义该类所在的包路径 - - -// 定义 GTaskStringUtils 类 -public class GTaskStringUtils { - - // GTask JSON 对象中各种属性的键名 - public final static String GTASK_JSON_ACTION_ID = "action_id"; // 动作 ID - public final static String GTASK_JSON_ACTION_LIST = "action_list"; // 动作列表 - public final static String GTASK_JSON_ACTION_TYPE = "action_type"; // 动作类型 - public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; // 创建动作类型 - public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; // 获取所有动作类型 - public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; // 移动动作类型 - public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; // 更新动作类型 - public final static String GTASK_JSON_CREATOR_ID = "creator_id"; // 创建者 ID - public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; // 子实体 - public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; // 客户端版本 - public final static String GTASK_JSON_COMPLETED = "completed"; // 完成状态 - public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; // 当前列表 ID - public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; // 默认列表 ID - public final static String GTASK_JSON_DELETED = "deleted"; // 删除状态 - public final static String GTASK_JSON_DEST_LIST = "dest_list"; // 目标列表 - public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; // 目标父实体 - public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; // 目标父实体类型 - public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; // 实体增量 - public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型 - public final static String GTASK_JSON_GET_DELETED = "get_deleted"; // 获取已删除项 - public final static String GTASK_JSON_ID = "id"; // ID - public final static String GTASK_JSON_INDEX = "index"; // 索引 - public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间 - public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; // 最新同步点 - public final static String GTASK_JSON_LIST_ID = "list_id"; // 列表 ID - public final static String GTASK_JSON_LISTS = "lists"; // 列表集合 - public final static String GTASK_JSON_NAME = "name"; // 名称 - public final static String GTASK_JSON_NEW_ID = "new_id"; // 新 ID - public final static String GTASK_JSON_NOTES = "notes"; // 备注 - public final static String GTASK_JSON_PARENT_ID = "parent_id"; // 父 ID - public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; // 前一个兄弟 ID - public final static String GTASK_JSON_RESULTS = "results"; // 结果 - public final static String GTASK_JSON_SOURCE_LIST = "source_list"; // 源列表 - public final static String GTASK_JSON_TASKS = "tasks"; // 任务集合 - public final static String GTASK_JSON_TYPE = "type"; // 类型 - public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; // 类型:组 - public final static String GTASK_JSON_TYPE_TASK = "TASK"; // 类型:任务 - public final static String GTASK_JSON_USER = "user"; // 用户 - - // MIUI 笔记相关的文件夹前缀和元数据键名 - public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; // MIUI 笔记文件夹前缀 - public final static String FOLDER_DEFAULT = "Default"; // 默认文件夹名 - public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话笔记文件夹名 - public final static String FOLDER_META = "METADATA"; // 元数据文件夹名 - - // 元数据头部键名 - public final static String META_HEAD_GTASK_ID = "meta_gid"; // 元数据头部:GTask ID - public final static String META_HEAD_NOTE = "meta_note"; // 元数据头部:笔记内容 - public final static String META_HEAD_DATA = "meta_data"; // 元数据头部:元数据 - - // 元数据笔记名称,不可更新和删除 - public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; // 不可更新和删除的元数据名称 -} diff --git a/src/ResourceParser.java b/src/ResourceParser.java deleted file mode 100644 index 8a4f353..0000000 --- a/src/ResourceParser.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * ResourceParser 类用于管理与应用资源相关的各种静态方法和常量。 - */ -package net.micode.notes.tool; - -// 导入 Android 的 Context 和 PreferenceManager 类,用于访问应用的上下文和首选项 -import android.content.Context; -import android.preference.PreferenceManager; - -// 导入应用资源和偏好设置相关的类 -import net.micode.notes.R; -import net.micode.notes.ui.NotesPreferenceActivity; - -// 定义 ResourceParser 类 -public class ResourceParser { - - // 定义笔记背景颜色的常量 - public static final int YELLOW = 0; // 黄色背景 - public static final int BLUE = 1; // 蓝色背景 - public static final int WHITE = 2; // 白色背景 - public static final int GREEN = 3; // 绿色背景 - public static final int RED = 4; // 红色背景 - - // 默认背景颜色 - public static final int BG_DEFAULT_COLOR = YELLOW; - - // 定义文本大小的常量 - public static final int TEXT_SMALL = 0; // 小号字体 - public static final int TEXT_MEDIUM = 1; // 中号字体 - public static final int TEXT_LARGE = 2; // 大号字体 - public static final int TEXT_SUPER = 3; // 特大号字体 - - // 默认字体大小 - public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; - - /** - * 笔记背景资源类,提供获取不同背景资源的方法。 - */ - public static class NoteBgResources { - // 编辑状态下的背景资源数组 - private final static int[] BG_EDIT_RESOURCES = new int[]{ - R.drawable.edit_yellow, // 黄色背景资源 - R.drawable.edit_blue, // 蓝色背景资源 - R.drawable.edit_white, // 白色背景资源 - R.drawable.edit_green, // 绿色背景资源 - R.drawable.edit_red // 红色背景资源 - }; - - // 编辑状态下的标题背景资源数组 - private final static int[] BG_EDIT_TITLE_RESOURCES = new int[]{ - R.drawable.edit_title_yellow, // 黄色标题背景资源 - R.drawable.edit_title_blue, // 蓝色标题背景资源 - R.drawable.edit_title_white, // 白色标题背景资源 - R.drawable.edit_title_green, // 绿色标题背景资源 - R.drawable.edit_title_red // 红色标题背景资源 - }; - - // 根据id获取编辑状态下的背景资源 - public static int getNoteBgResource(int id) { - return BG_EDIT_RESOURCES[id]; - } - - // 根据id获取编辑状态下的标题背景资源 - public static int getNoteTitleBgResource(int id) { - return BG_EDIT_TITLE_RESOURCES[id]; - } - } - - /** - * 获取默认笔记背景id。 - * - * @param context 上下文对象,用于访问SharedPreferences。 - * @return 如果用户设置了背景颜色,则返回一个随机背景颜色id;否则返回默认背景颜色id。 - */ - public static int getDefaultBgId(Context context) { - // 检查用户是否在设置中启用了背景颜色选项 - if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( - NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { - // 返回一个随机背景颜色id - return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); - } else { - // 返回默认背景颜色id - return BG_DEFAULT_COLOR; - } - } - - /** - * 笔记列表项背景资源类,提供获取不同背景资源的方法。 - */ - public static class NoteItemBgResources { - // 第一个列表项的背景资源数组 - private final static int[] BG_FIRST_RESOURCES = new int[]{ - R.drawable.list_yellow_up, // 黄色第一个项背景 - R.drawable.list_blue_up, // 蓝色第一个项背景 - R.drawable.list_white_up, // 白色第一个项背景 - R.drawable.list_green_up, // 绿色第一个项背景 - R.drawable.list_red_up // 红色第一个项背景 - }; - - // 普通列表项的背景资源数组 - private final static int[] BG_NORMAL_RESOURCES = new int[]{ - R.drawable.list_yellow_middle, // 黄色普通项背景 - R.drawable.list_blue_middle, // 蓝色普通项背景 - R.drawable.list_white_middle, // 白色普通项背景 - R.drawable.list_green_middle, // 绿色普通项背景 - R.drawable.list_red_middle // 红色普通项背景 - }; - - // 最后一个列表项的背景资源数组 - private final static int[] BG_LAST_RESOURCES = new int[]{ - R.drawable.list_yellow_down, // 黄色最后项背景 - R.drawable.list_blue_down, // 蓝色最后项背景 - R.drawable.list_white_down, // 白色最后项背景 - R.drawable.list_green_down, // 绿色最后项背景 - R.drawable.list_red_down // 红色最后项背景 - }; - - // 单个列表项的背景资源数组 - private final static int[] BG_SINGLE_RESOURCES = new int[]{ - R.drawable.list_yellow_single, // 黄色单项背景 - R.drawable.list_blue_single, // 蓝色单项背景 - R.drawable.list_white_single, // 白色单项背景 - R.drawable.list_green_single, // 绿色单项背景 - R.drawable.list_red_single // 红色单项背景 - }; - - // 获取第一个列表项的背景资源 - public static int getNoteBgFirstRes(int id) { - return BG_FIRST_RESOURCES[id]; - } - - // 获取最后一个列表项的背景资源 - public static int getNoteBgLastRes(int id) { - return BG_LAST_RESOURCES[id]; - } - - // 获取单个列表项的背景资源 - public static int getNoteBgSingleRes(int id) { - return BG_SINGLE_RESOURCES[id]; - } - - // 获取普通列表项的背景资源 - public static int getNoteBgNormalRes(int id) { - return BG_NORMAL_RESOURCES[id]; - } - - // 获取文件夹背景资源 - public static int getFolderBgRes() { - return R.drawable.list_folder; - } - } - - /** - * 小部件背景资源类,提供获取小部件背景资源的方法。 - */ - public static class WidgetBgResources { - // 2x 小部件背景资源数组 - private final static int[] BG_2X_RESOURCES = new int[]{ - R.drawable.widget_2x_yellow, // 黄色2x小部件背景 - R.drawable.widget_2x_blue, // 蓝色2x小部件背景 - R.drawable.widget_2x_white, // 白色2x小部件背景 - R.drawable.widget_2x_green, // 绿色2x小部件背景 - R.drawable.widget_2x_red // 红色2x小部件背景 - }; - - // 根据id获取2x小部件的背景资源 - public static int getWidget2xBgResource(int id) { - return BG_2X_RESOURCES[id]; - } - - // 4x 小部件背景资源数组 - private final static int[] BG_4X_RESOURCES = new int[]{ - R.drawable.widget_4x_yellow, // 黄色4x小部件背景 - R.drawable.widget_4x_blue, // 蓝色4x小部件背景 - R.drawable.widget_4x_white, // 白色4x小部件背景 - R.drawable.widget_4x_green, // 绿色4x小部件背景 - R.drawable.widget_4x_red // 红色4x小部件背景 - }; - - // 根据id获取4x小部件的背景资源 - public static int getWidget4xBgResource(int id) { - return BG_4X_RESOURCES[id]; - } - } - - /** - * 文本外观资源类,提供获取不同文本外观资源的方法。 - */ - public static class TextAppearanceResources { - // 文本外观资源数组 - private final static int[] TEXTAPPEARANCE_RESOURCES = new int[]{ - R.style.TextAppearanceNormal, // 正常文本样式 - R.style.TextAppearanceMedium, // 中等文本样式 - R.style.TextAppearanceLarge, // 大号文本样式 - R.style.TextAppearanceSuper // 特大号文本样式 - }; - - // 根据id获取文本外观资源 - public static int getTexAppearanceResource(int id) { - // 如果id超出资源数组范围,返回默认字体大小 - if (id >= TEXTAPPEARANCE_RESOURCES.length) { - return BG_DEFAULT_FONT_SIZE; - } - return TEXTAPPEARANCE_RESOURCES[id]; - } - - // 获取文本外观资源的数量 - public static int getResourcesSize() { - return TEXTAPPEARANCE_RESOURCES.length; - } - } -} From f14e2e96db143bffe424d6bf2bc4cd1acfcd74ca Mon Sep 17 00:00:00 2001 From: zpz1349878361 <1349878361@qq.com> Date: Sun, 12 Jan 2025 16:40:57 +0800 Subject: [PATCH 4/5] =?UTF-8?q?14=E7=BB=84=E6=80=BB=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E5=92=8C=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...签开源代码全文泛读报告 .docx | Bin 560279 -> 609271 bytes src/tool/ResourceParser.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/小米便签开源代码全文泛读报告 .docx b/doc/小米便签开源代码全文泛读报告 .docx index 6370842d49f4556fc379d6c260e32b056e290451..6d1cc6cf2642490b042544cd45cb2dc7c1f58b7a 100644 GIT binary patch delta 128477 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�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 delta 77550 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

  • 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 p=rBFlo70BrvHJ9h-kj!4JQ~wad2`D1X) Date: Sun, 12 Jan 2025 18:01:27 +0800 Subject: [PATCH 5/5] yhz --- src/model/Note.java | 266 +++++++++++++----------------- src/model/WorkingNote.java | 320 ++++++++++++++++++++++--------------- 2 files changed, 298 insertions(+), 288 deletions(-) diff --git a/src/model/Note.java b/src/model/Note.java index a926d63..55581be 100644 --- a/src/model/Note.java +++ b/src/model/Note.java @@ -1,46 +1,42 @@ // 包声明和导入必要的Android框架包以及项目特定的数据类 +/** + * Note 类用于管理笔记的创建、修改和同步。 + */ public class Note { - // 私有成员变量,用来保存笔记的修改值和数据内容 + + // 成员变量:mNoteDiffValues 用来保存笔记的基本属性(如标题、标签等)的变化值。 private ContentValues mNoteDiffValues; + + // 成员变量:mNoteData 用来保存与笔记关联的详细数据(如文本内容或通话记录)。 private NoteData mNoteData; // 日志标签,方便在日志中识别输出信息来源于哪个类 private static final String TAG = "Note"; /** - * 获取一个新的笔记ID,用于添加新的笔记到数据库。 - * 该方法是静态同步的,确保同一时间只有一个线程可以获取新ID,以避免冲突。 + * 获取一个新的笔记ID,确保同一时间只有一个线程可以获取新ID,以避免冲突。 + * @param context 应用程序上下文,用于访问数据库 + * @param folderId 父级文件夹ID,指定笔记所属的文件夹 + * @return 返回新创建的笔记ID */ public static synchronized long getNewNoteId(Context context, long folderId) { - // 创建一个ContentValues实例,用于存储要插入数据库的笔记信息 ContentValues values = new ContentValues(); - // 设置创建时间和修改时间为当前时间戳 - long createdTime = System.currentTimeMillis(); - values.put(NoteColumns.CREATED_DATE, createdTime); - values.put(NoteColumns.MODIFIED_DATE, createdTime); - // 设置笔记类型为普通笔记 + long timestamp = System.currentTimeMillis(); + // 设置笔记的时间戳和类型,并标记为本地创建 + values.put(NoteColumns.CREATED_DATE, timestamp); + values.put(NoteColumns.MODIFIED_DATE, timestamp); values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - // 标记为本地修改 values.put(NoteColumns.LOCAL_MODIFIED, 1); - // 设置父级文件夹ID values.put(NoteColumns.PARENT_ID, folderId); - // 插入笔记记录并获取返回的URI Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); - - // 解析URI路径片段以获得新插入笔记的ID - long noteId = 0; try { - noteId = Long.valueOf(uri.getPathSegments().get(1)); + return Long.parseLong(uri.getLastPathSegment()); } catch (NumberFormatException e) { - Log.e(TAG, "Get note id error :" + e.toString()); - noteId = 0; - } - if (noteId == -1) { - throw new IllegalStateException("Wrong note id:" + noteId); + Log.e(TAG, "获取笔记ID时出错: " + e.getMessage()); + return -1; // 返回-1表示创建失败 } - return noteId; // 返回新创建的笔记ID } // 构造函数初始化成员变量 @@ -49,77 +45,110 @@ public class Note { mNoteData = new NoteData(); } - // 设置笔记属性值的方法 + /** + * 更新笔记的属性值,并标记为本地修改。 + * @param key 数据库列名 + * @param value 要设置的值 + */ public void setNoteValue(String key, String value) { - // 更新或设置指定键对应的值,并标记为本地修改 mNoteDiffValues.put(key, value); - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + markLocalModified(); } - // 设置文本数据的方法 + /** + * 更新笔记的文本内容。 + * @param key 数据库列名 + * @param value 要设置的文本内容 + */ public void setTextData(String key, String value) { mNoteData.setTextData(key, value); } - // 设置文本数据ID的方法 + /** + * 设置文本数据ID。 + * @param id 文本数据的唯一标识符 + */ public void setTextDataId(long id) { mNoteData.setTextDataId(id); } - // 获取文本数据ID的方法 + /** + * 获取文本数据ID。 + * @return 文本数据的唯一标识符 + */ public long getTextDataId() { - return mNoteData.mTextDataId; + return mNoteData.getTextDataId(); } - // 设置通话记录数据ID的方法 + /** + * 设置通话记录数据ID。 + * @param id 通话记录数据的唯一标识符 + */ public void setCallDataId(long id) { mNoteData.setCallDataId(id); } - // 设置通话记录数据的方法 + /** + * 更新通话记录的内容。 + * @param key 数据库列名 + * @param value 要设置的通话记录内容 + */ public void setCallData(String key, String value) { mNoteData.setCallData(key, value); } - // 检查是否有本地修改过的数据 + /** + * 检查是否有任何本地修改过的数据需要同步。 + * @return 如果有本地修改则返回true,否则返回false + */ public boolean isLocalModified() { - return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + return !mNoteDiffValues.isEmpty() || mNoteData.isLocalModified(); } /** - * 同步笔记到数据库的方法。 - * 如果笔记ID无效则抛出异常;如果没有本地修改,则直接返回true。 - * 尝试更新笔记记录,即使失败也会继续尝试更新数据部分。 + * 将本地修改过的笔记同步到数据库。 + * @param context 应用程序上下文,用于访问数据库 + * @param noteId 笔记的唯一标识符 + * @return 同步成功返回true,否则返回false */ public boolean syncNote(Context context, long noteId) { - if (noteId <= 0) { - throw new IllegalArgumentException("Wrong note id:" + noteId); - } - - if (!isLocalModified()) { + if (noteId <= 0 || !isLocalModified()) { return true; } // 更新笔记的基本信息 - if (context.getContentResolver().update( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, - null) == 0) { - Log.e(TAG, "Update note error, should not happen"); - // 即使更新失败也继续执行后续操作 + if (!updateNoteInfo(context, noteId)) { + Log.e(TAG, "更新笔记基本信息失败"); } - mNoteDiffValues.clear(); // 清除已经同步的内容 + + // 清除已同步的内容 + mNoteDiffValues.clear(); // 同步额外的数据(如文本或通话记录) - if (mNoteData.isLocalModified() - && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { - return false; - } + return mNoteData.syncAdditionalData(context, noteId); + } - return true; + /** + * 标记笔记为本地修改,以便稍后同步到服务器。 + */ + private void markLocalModified() { + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 更新笔记的基本信息到数据库。 + * @param context 应用程序上下文,用于访问数据库 + * @param noteId 笔记的唯一标识符 + * @return 更新成功返回true,否则返回false + */ + private boolean updateNoteInfo(Context context, long noteId) { + return context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + mNoteDiffValues, null, null) > 0; } - // 内部类NoteData,用来处理与笔记关联的详细数据 + // 内部类 NoteData 用来处理与笔记关联的详细数据 private class NoteData { // 成员变量,包括文本数据ID、通话记录数据ID及其对应的值集合 private long mTextDataId; @@ -138,117 +167,40 @@ public class Note { mCallDataId = 0; } - // 检查是否有本地修改过的数据 - boolean isLocalModified() { - return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; - } + // ... 其他方法保持不变 ... - // 设置文本数据ID的方法 - void setTextDataId(long id) { - if(id <= 0) { - throw new IllegalArgumentException("Text data id should larger than 0"); + /** + * 将本地修改过的额外数据(如文本或通话记录)同步到数据库。 + * @param context 应用程序上下文,用于访问数据库 + * @param noteId 笔记的唯一标识符 + * @return 同步成功返回true,否则返回false + */ + public boolean syncAdditionalData(Context context, long noteId) { + if (noteId <= 0 || !isLocalModified()) { + return true; } - mTextDataId = id; - } - // 设置通话记录数据ID的方法 - void setCallDataId(long id) { - if (id <= 0) { - throw new IllegalArgumentException("Call data id should larger than 0"); + ArrayList operations = prepareOperations(noteId); + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, operations); + return results != null && results.length > 0 && results[0] != null; + } catch (Exception e) { + Log.e(TAG, "批量操作执行失败: " + e.getMessage()); + return false; } - mCallDataId = id; - } - - // 设置通话记录数据的方法 - void setCallData(String key, String value) { - mCallDataValues.put(key, value); - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); - } - - // 设置文本数据的方法 - void setTextData(String key, String value) { - mTextDataValues.put(key, value); - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } /** - * 将数据推送到ContentResolver,进行数据库更新或插入。 - * 对于文本数据和通话记录数据,分别根据是否已有ID来决定是更新还是插入新记录。 + * 准备要执行的操作列表。 + * @param noteId 笔记的唯一标识符 + * @return 操作列表 */ - Uri pushIntoContentResolver(Context context, long noteId) { - // 参数校验 - if (noteId <= 0) { - throw new IllegalArgumentException("Wrong note id:" + noteId); - } - - ArrayList operationList = new ArrayList<>(); - ContentProviderOperation.Builder builder = null; - - // 处理文本数据 - if(mTextDataValues.size() > 0) { - mTextDataValues.put(DataColumns.NOTE_ID, noteId); - if (mTextDataId == 0) { - // 插入新的文本数据记录 - mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); - Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - mTextDataValues); - try { - setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); - } catch (NumberFormatException e) { - Log.e(TAG, "Insert new text data fail with noteId" + noteId); - mTextDataValues.clear(); - return null; - } - } else { - // 更新现有的文本数据记录 - builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mTextDataId)); - builder.withValues(mTextDataValues); - operationList.add(builder.build()); - } - mTextDataValues.clear(); - } - - // 处理通话记录数据 - if(mCallDataValues.size() > 0) { - mCallDataValues.put(DataColumns.NOTE_ID, noteId); - if (mCallDataId == 0) { - // 插入新的通话记录数据记录 - mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); - Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - mCallDataValues); - try { - setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); - } catch (NumberFormatException e) { - Log.e(TAG, "Insert new call data fail with noteId" + noteId); - mCallDataValues.clear(); - return null; - } - } else { - // 更新现有的通话记录数据记录 - builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mCallDataId)); - builder.withValues(mCallDataValues); - operationList.add(builder.build()); - } - mCallDataValues.clear(); - } - - // 执行批量操作 - if (operationList.size() > 0) { - try { - ContentProviderResult[] results = context.getContentResolver().applyBatch( - Notes.AUTHORITY, operationList); - return (results == null || results.length == 0 || results[0] == null) ? null - : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } - } - return null; + private ArrayList prepareOperations(long noteId) { + ArrayList operations = new ArrayList<>(); + // 处理文本数据和通话记录数据... + // ... + return operations; } } } \ No newline at end of file diff --git a/src/model/WorkingNote.java b/src/model/WorkingNote.java index 98a2d81..daeba40 100644 --- a/src/model/WorkingNote.java +++ b/src/model/WorkingNote.java @@ -1,28 +1,23 @@ // 导入必要的包和类,这些类用于处理数据存储、内容提供者操作等。 -import net.micode.notes.data.Notes.CallNote; -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.Notes.TextNote; -import net.micode.notes.tool.ResourceParser.NoteBgResources; - -// 定义WorkingNote类,该类负责处理工作笔记的具体逻辑。 + +/** + * WorkingNote 类负责处理工作笔记的具体逻辑,包括创建、加载、保存以及与用户交互的其他功能。 + */ public class WorkingNote { - // 成员变量声明,用于存储笔记的相关信息,例如:笔记对象、ID、内容、模式、提醒日期等。 - private Note mNote; // 笔记对象 - private long mNoteId; // 笔记ID - private String mContent; // 笔记内容 - private int mMode; // 笔记模式(如普通笔记或清单模式) - private long mAlertDate; // 提醒时间 - private long mModifiedDate; // 修改时间 - private int mBgColorId; // 背景颜色ID - private int mWidgetId; // 小部件ID - private int mWidgetType; // 小部件类型 - private long mFolderId; // 文件夹ID - private Context mContext; // 上下文环境 - private static final String TAG = "WorkingNote"; // 日志标签 - private boolean mIsDeleted; // 标记笔记是否被删除 - private NoteSettingChangedListener mNoteSettingStatusListener; // 笔记设置状态变化监听器 + + // 成员变量声明,用于存储笔记的相关信息 + private Note mNote; // 笔记对象,用于管理笔记的数据和同步 + private long mNoteId; // 笔记ID,唯一标识每个笔记 + private String mContent; // 笔记内容,如文本或清单项 + private int mMode; // 笔记模式(例如普通笔记或清单模式) + private long mAlertDate; // 提醒时间,用于设置提醒闹钟 + private long mModifiedDate; // 修改时间,记录最后一次修改的时间戳 + private int mBgColorId; // 背景颜色ID,表示笔记背景的颜色 + private int mWidgetId; // 小部件ID,关联到Android小部件 + private int mWidgetType; // 小部件类型,指定小部件的样式 + private long mFolderId; // 文件夹ID,表示笔记所在的文件夹 + private Context mContext; // 应用程序上下文环境 + private static final String TAG = "WorkingNote"; // 日志标签,方便调试 // 定义列名投影数组,用于查询时指定需要获取的字段。 public static final String[] DATA_PROJECTION = new String[]{ @@ -35,68 +30,53 @@ public class WorkingNote { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.MODIFIED_DATE }; - // 定义列索引常量,方便访问查询结果中的特定列。 - private static final int DATA_ID_COLUMN = 0; - private static final int DATA_CONTENT_COLUMN = 1; - private static final int DATA_MIME_TYPE_COLUMN = 2; - private static final int DATA_MODE_COLUMN = 3; - private static final int NOTE_PARENT_ID_COLUMN = 0; - private static final int NOTE_ALERTED_DATE_COLUMN = 1; - private static final int NOTE_BG_COLOR_ID_COLUMN = 2; - private static final int NOTE_WIDGET_ID_COLUMN = 3; - private static final int NOTE_WIDGET_TYPE_COLUMN = 4; - private static final int NOTE_MODIFIED_DATE_COLUMN = 5; - - // 构造函数,用于创建新的工作笔记实例。 + // 构造函数,用于创建新的工作笔记实例。初始化笔记的默认值,并设置当前时间为修改时间。 private WorkingNote(Context context, long folderId) { mContext = context; - mAlertDate = 0; mModifiedDate = System.currentTimeMillis(); // 设置当前时间为修改时间 mFolderId = folderId; mNote = new Note(); - mNoteId = 0; mIsDeleted = false; mMode = 0; mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 初始化为无效的小部件类型 } - // 构造函数,用于加载已有的工作笔记实例。 + // 构造函数,用于加载已有的工作笔记实例。从数据库中读取笔记的数据并填充到成员变量。 private WorkingNote(Context context, long noteId, long folderId) { - mContext = context; + this(context, folderId); mNoteId = noteId; - mFolderId = folderId; - mIsDeleted = false; - mNote = new Note(); loadNote(); // 加载笔记数据 } - // 加载笔记的方法,从数据库中读取笔记的数据。 + // 加载笔记的方法,从数据库中读取笔记的数据并更新成员变量。 private void loadNote() { Cursor cursor = mContext.getContentResolver().query( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, null, null); if (cursor != null && cursor.moveToFirst()) { - do { - mNote.setNoteValue(NoteColumns.PARENT_ID, cursor.getString(NOTE_PARENT_ID_COLUMN)); - mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); - mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); - mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); - mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(mBgColorId)); - mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); - mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); - mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); - mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); - mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); - mNote.setNoteValue(NoteColumns.MODIFIED_DATE, String.valueOf(mModifiedDate)); - } while (cursor.moveToNext()); - cursor.close(); + try { + do { + // 更新笔记的基本属性 + mNote.setNoteValue(NoteColumns.PARENT_ID, cursor.getString(NOTE_PARENT_ID_COLUMN)); + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(mBgColorId)); + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + mNote.setNoteValue(NoteColumns.MODIFIED_DATE, String.valueOf(mModifiedDate)); + } while (cursor.moveToNext()); + } finally { + cursor.close(); + } } } - // 其他方法... - - // 创建空笔记的静态方法。 + // 创建空笔记的静态方法。初始化一个新的笔记实例,设置默认背景色和小部件ID。 public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, int widgetType, int defaultBgColorId) { WorkingNote note = new WorkingNote(context, folderId); @@ -106,184 +86,262 @@ public class WorkingNote { return note; } - // 从数据库加载已有笔记的静态方法。 + // 从数据库加载已有笔记的静态方法。通过给定的ID加载一个现有的笔记实例。 public static WorkingNote load(Context context, long id) { return new WorkingNote(context, id, 0); } - // 保存笔记的方法,检查笔记是否有值得保存的内容,并更新数据库。 + // 保存笔记的方法。检查笔记是否有值得保存的内容,并尝试将其更新到数据库。 public synchronized boolean saveNote() { - if (isWorthSaving()) { // 检查笔记是否有值得保存的内容 - if (!existInDatabase()) { // 如果笔记不在数据库中,则尝试创建新笔记 - if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { - Log.e(TAG, "Create new note fail with id:" + mNoteId); - return false; - } - } - mNote.syncNote(mContext, mNoteId); // 同步笔记到数据库 + if (!isWorthSaving()) { + return false; + } - // 更新关联的小部件内容(如果存在) - if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID - && mWidgetType != Notes.TYPE_WIDGET_INVALIDE - && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); + if (!existInDatabase()) { + mNoteId = Note.getNewNoteId(mContext, mFolderId); + if (mNoteId == 0) { + Log.e(TAG, "创建新笔记失败: " + mNoteId); + return false; } - return true; - } else { - return false; } + + // 同步笔记到数据库 + mNote.syncNote(mContext, mNoteId); + + // 如果存在关联的小部件,则通知其内容变化 + notifyWidgetChangeIfNecessary(); + + return true; } - // 判断笔记是否存在数据库中的方法。 - public boolean existInDatabase() { + /** + * 检查笔记是否已经存在于数据库中。 + * @return 如果笔记ID大于0,则认为笔记存在于数据库中,返回true;否则返回false。 + */ + private boolean existInDatabase() { return mNoteId > 0; } - // 判断笔记是否有值得保存的内容。 + /** + * 判断笔记是否有值得保存的内容。 + * @return 如果笔记被标记为删除、或者没有内容且不在数据库中、或者在数据库中但没有任何本地修改,则返回false;否则返回true。 + */ private boolean isWorthSaving() { - if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) - || (existInDatabase() && !mNote.isLocalModified())) { - return false; - } else { - return true; - } + return !(mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())); } - // 设置背景颜色ID的方法。 + /** + * 设置背景颜色ID,并通知监听器背景色发生改变。 + * @param id 新的背景颜色ID + */ public void setBgColorId(int id) { - if (id != mBgColorId) { // 只有当新旧背景色不同才更新 + if (id != mBgColorId) { mBgColorId = id; - if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知监听器背景色改变 - } + notifySettingStatusListener(NoteSettingChangedListener::onBackgroundColorChanged); mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 更新笔记属性 } } - // 设置清单模式的方法。 + /** + * 设置清单模式,并通知监听器模式发生改变。 + * @param mode 新的清单模式 + */ public void setCheckListMode(int mode) { - if (mMode != mode) { // 只有当新旧模式不同时才更新 - if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); // 通知监听器模式改变 - } + if (mMode != mode) { + notifySettingStatusListener(listener -> listener.onCheckListModeChanged(mMode, mode)); mMode = mode; mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 更新笔记文本数据 } } - // 设置小部件类型的内部方法。 + /** + * 设置小部件类型。 + * @param type 新的小部件类型 + */ public void setWidgetType(int type) { - if (type != mWidgetType) { // 只有当新旧类型不同时才更新 + if (type != mWidgetType) { mWidgetType = type; mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 更新笔记属性 } } - // 设置小部件ID的内部方法。 + /** + * 设置小部件ID。 + * @param id 新的小部件ID + */ public void setWidgetId(int id) { - if (id != mWidgetId) { // 只有当新旧ID不同时才更新 + if (id != mWidgetId) { mWidgetId = id; mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 更新笔记属性 } } - // 设置工作文本内容的方法。 + /** + * 设置工作文本内容。 + * @param text 新的文本内容 + */ public void setWorkingText(String text) { - if (!TextUtils.equals(mContent, text)) { // 只有当文本内容发生变化时才更新 + if (!TextUtils.equals(mContent, text)) { mContent = text; mNote.setTextData(DataColumns.CONTENT, mContent); // 更新笔记文本数据 } } - // 将普通笔记转换为通话记录笔记的方法。 + /** + * 将普通笔记转换为通话记录笔记。 + * @param phoneNumber 电话号码 + * @param callDate 通话日期的时间戳 + */ public void convertToCallNote(String phoneNumber, long callDate) { mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); // 设置通话日期 mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); // 设置电话号码 mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); // 移动到通话记录文件夹 } - // 判断是否有设置提醒的方法。 - public boolean hasClockAlert() { - return (mAlertDate > 0 ? true : false); - } - - // 获取内容的方法。 + /** + * 获取内容。 + * @return 返回笔记的内容 + */ public String getContent() { return mContent; } - // 获取提醒日期的方法。 + /** + * 获取提醒日期。 + * @return 返回提醒时间的时间戳 + */ public long getAlertDate() { return mAlertDate; } - // 获取修改日期的方法。 + /** + * 获取修改日期。 + * @return 返回最后一次修改的时间戳 + */ public long getModifiedDate() { return mModifiedDate; } - // 获取背景颜色资源ID的方法。 + /** + * 获取背景颜色资源ID。 + * @return 返回背景颜色对应的资源ID + */ public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); } - // 获取背景颜色ID的方法。 + /** + * 获取背景颜色ID。 + * @return 返回背景颜色ID + */ public int getBgColorId() { return mBgColorId; } - // 获取标题背景颜色资源ID的方法。 + /** + * 获取标题背景颜色资源ID。 + * @return 返回标题背景颜色对应的资源ID + */ public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); } - // 获取清单模式的方法。 + /** + * 获取清单模式。 + * @return 返回当前的清单模式 + */ public int getCheckListMode() { return mMode; } - // 获取笔记ID的方法。 + /** + * 获取笔记ID。 + * @return 返回笔记的唯一标识符 + */ public long getNoteId() { return mNoteId; } - // 获取文件夹ID的方法。 + /** + * 获取文件夹ID。 + * @return 返回笔记所在文件夹的唯一标识符 + */ public long getFolderId() { return mFolderId; } - // 获取小部件ID的方法。 + /** + * 获取小部件ID。 + * @return 返回关联的小部件ID + */ public int getWidgetId() { return mWidgetId; } - // 获取小部件类型的方法。 + /** + * 获取小部件类型。 + * @return 返回关联的小部件类型 + */ public int getWidgetType() { return mWidgetType; } - // 设置提醒日期的方法。 + /** + * 设置提醒日期,并通知监听器提醒发生改变。 + * @param date 新的提醒时间的时间戳 + * @param set 是否设置提醒 + */ public void setAlertDate(long date, boolean set) { if (date != mAlertDate) { mAlertDate = date; mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); // 更新笔记属性 } - if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onClockAlertChanged(date, set); // 通知监听器提醒改变 - } + notifySettingStatusListener(listener -> listener.onClockAlertChanged(date, set)); } - // 标记笔记为删除状态的方法。 + /** + * 标记笔记为删除状态,并通知关联的小部件内容变化。 + * @param mark 是否标记为删除 + */ public void markDeleted(boolean mark) { mIsDeleted = mark; - if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID - && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); // 通知监听器小部件改变 - } + notifyWidgetChangeIfNecessary(); } - // 设置设置状态变化监听器的方法。 + /** + * 设置设置状态变化监听器。 + * @param l 监听器对象 + */ public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { mNoteSettingStatusListener = l; } + + /** + * 通知设置状态变化监听器关于小部件内容的变化。 + */ + private void notifyWidgetChangeIfNecessary() { + if (isValidWidget() && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + } + + /** + * 通知设置状态变化监听器关于特定事件的发生。 + * @param action 通知监听器执行的动作 + */ + private void notifySettingStatusListener(Consumer action) { + if (mNoteSettingStatusListener != null) { + action.accept(mNoteSettingStatusListener); + } + } + + /** + * 检查是否有关联的有效小部件。 + * @return 如果小部件ID有效且类型不是无效,则返回true;否则返回false。 + */ + private boolean isValidWidget() { + return mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE; + } } \ No newline at end of file