From b3c26d8844cac0fae0a1dc69cf4ae59912152c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Sat, 23 Nov 2024 19:03:07 +0800 Subject: [PATCH 01/44] Signed-off-by: --- fandubaogao.docx | Bin 0 -> 202584 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 fandubaogao.docx diff --git a/fandubaogao.docx b/fandubaogao.docx new file mode 100644 index 0000000000000000000000000000000000000000..5c0fe803274d98ada42249049e505faf89ef3467 GIT binary patch literal 202584 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

k5Pa{J>uiz)1U#?pOC?2U9f658FItx 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

BTLei(e`3n#~2CYr$USmS><8=Vq4<)>Z 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

T;PZ9sU!u6HBXM`>XL_*HLi=rs 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 literal 0 HcmV?d00001 -- 2.34.1 From d34dd0852df780f5aea141aef4e3f159073de9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:14:43 +0800 Subject: [PATCH 02/44] Signed-off-by: --- Contact.java | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Contact.java diff --git a/Contact.java b/Contact.java new file mode 100644 index 0000000..f0fccea --- /dev/null +++ b/Contact.java @@ -0,0 +1,81 @@ +/* + * 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.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 { //缓存联系人信息的HashMap,键为电话号码,值为联系人姓名 + private static HashMap sContactCache; //日志标签 + private static final String TAG = "Contact"; //查询联系人信息的SQL选择语句模板 + + 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); + } + //替换SQL选择语句中的占位符 + 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; + } + } +} -- 2.34.1 From 3b0176d56dd4f2772f74277962506bc006894b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:17:11 +0800 Subject: [PATCH 03/44] Signed-off-by: --- Notes.java | 441 ++++++++++++++++++++++++++++++++++++ NotesDatabaseHelper.java | 475 +++++++++++++++++++++++++++++++++++++++ NotesProvider.java | 375 +++++++++++++++++++++++++++++++ 3 files changed, 1291 insertions(+) create mode 100644 Notes.java create mode 100644 NotesDatabaseHelper.java create mode 100644 NotesProvider.java diff --git a/Notes.java b/Notes.java new file mode 100644 index 0000000..0d1d179 --- /dev/null +++ b/Notes.java @@ -0,0 +1,441 @@ +/* + * 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; +/* + * 定义了与笔记相关的常量和接口 + */ +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; //系统文件夹 + + /** + * Following IDs are system folders' identifiers + * {@link Notes#ID_ROOT_FOLDER } is default folder + * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder + * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + */ + /* + * 系统文件夹的标识符 + * {@link Notes#ID_ROOT_FOLDER }是默认文件夹 + * {@link Notes#ID_TEMPARAY_FOLDER }是用于没有所属文件夹的笔记 + * {@link Notes#ID_CALL_RECORD_FOLDER}是用于存储通话记录的文件夹 + */ + public static final int ID_ROOT_FOLDER = 0; + public static final int ID_TEMPARAY_FOLDER = -1; + public static final int ID_CALL_RECORD_FOLDER = -2; + public static final int ID_TRASH_FOLER = -3; + //用于Intent传递的额外数据键 + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_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"; + 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 to query all notes and folders + */ + /* + * Uri用于查询所有的笔记和文件夹 + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * Uri to query data + */ + /* + * Uri用于查询数据 + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + /* + * 笔记表的列定义 + */ + public interface NoteColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + /* + * 行的唯一ID + *

类型:INTEGER(long)

+ */ + public static final String ID = "_id"; + + /** + * The parent's id for note or folder + *

Type: INTEGER (long)

+ */ + /* + * 笔记或文件夹的父ID + *

类型:INTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + /* + * 创建日期 + *

类型:INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + /* + * 最后修改日期 + *

类型:INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + + /** + * Alert date + *

Type: INTEGER (long)

+ */ + /* + * 提醒日期 + *

类型:INTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * Folder's name or text content of note + *

Type: TEXT

+ */ + /* + * 文件夹的名称或笔记的文本内容 + *

类型:TEXT

+ */ + public static final String SNIPPET = "snippet"; + + /** + * Note's widget id + *

Type: INTEGER (long)

+ */ + /* + * 笔记的小部件ID + *

类型:INTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * Note's widget type + *

Type: INTEGER (long)

+ */ + /* + * 笔记的小部件类型 + *

类型:INTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * Note's background color's id + *

Type: INTEGER (long)

+ */ + /* + * 笔记的背景颜色ID + *

类型:INTEGER (long)

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * For text note, it doesn't has attachment, for multi-media + * note, it has at least one attachment + *

Type: INTEGER

+ */ + /* + * 是否包含组件 + *

类型:INTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * Folder's count of notes + *

Type: INTEGER (long)

+ */ + /* + * 文件夹中的笔记数量 + *

类型:INTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * The file type: folder or note + *

Type: INTEGER

+ */ + /* + * 文件类型:文件夹或笔记 + *

类型:INTEGER

+ */ + public static final String TYPE = "type"; + + /** + * The last sync id + *

Type: INTEGER (long)

+ */ + /* + * 最后一次同步的ID + *

类型:INTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * Sign to indicate local modified or not + *

Type: INTEGER

+ */ + /* + * 指示是否在本地被修改 + *

类型:INTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * Original parent id before moving into temporary folder + *

Type : INTEGER

+ */ + /* + * 移动到临时文件夹之前的原始父ID + *

类型:INTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * The gtask id + *

Type : TEXT

+ */ + /* + * gtask ID + *

类型:TEXT

+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * The version code + *

Type : INTEGER (long)

+ */ + /* + * 版本号 + *

类型:INTEGER (long)

+ */ + public static final String VERSION = "version"; + } + /* + * 数据表的列定义 + */ + public interface DataColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + /* + * 行的唯一ID + *

类型:INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The MIME type of the item represented by this row. + *

Type: Text

+ */ + /* + * 该行的MIME类型 + *

类型:TEXT

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * The reference id to note that this data belongs to + *

Type: INTEGER (long)

+ */ + /* + * 该数据所属的笔记ID + *

类型:INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + /* + * 创建日期 + *

类型:INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + /* + * 最后修改日期 + *

类型:INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * Data's content + *

Type: TEXT

+ */ + /* + * 数据内容 + *

类型:TEXT

+ */ + public static final String CONTENT = "content"; + + + /** + * Generic data column, the meaning is{@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + /* + * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于整数数据类型 + *

类型:INTEGER

+ */ + public static final String DATA1 = "data1"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + /* + * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于整数数据类型 + *

类型: INTEGER

+ */ + public static final String DATA2 = "data2"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + /* + * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于TEXT数据类型 + *

类型: TEXT

+ */ + public static final String DATA3 = "data3"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + /* + * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于TEXT数据类型 + *

类型: TEXT

+ */ + public static final String DATA4 = "data4"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + /* + * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于TEXT数据类型 + *

类型: TEXT

+ */ + public static final String DATA5 = "data5"; + } + /* + * 文本笔记的定义 + */ + public static final class TextNote implements DataColumns { + /** + * Mode to indicate the text in check list mode or not + *

Type: Integer 1:check list mode 0: normal mode

+ */ + /* + * 模式,指示文本是否在检查列表模式 + *

类型: 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"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + } + /* + * 通话笔记的定义 + */ + public static final class CallNote implements DataColumns { + /** + * Call date for this record + *

Type: INTEGER (long)

+ */ + /* + * 通话日期 + *

类型: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; + + /** + * Phone number for this record + *

Type: TEXT

+ */ + /* + * 电话号码 + *

类型: TEXT

+ */ + + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + } +} + +/* + * 版权声明:保留了原有的版权声明,说明代码的许可协议和版权信息。 + + 常量定义:解释了各个常量的用途,如笔记类型、系统文件夹ID、Intent传递的额外数据键等。 + + 接口定义:详细说明了各个接口中的列定义,解释了每个列的用途和数据类型。 + + 类定义:解释了 TextNote 和 CallNote 类的用途,并详细说明了它们的数据列和常量。 + */ diff --git a/NotesDatabaseHelper.java b/NotesDatabaseHelper.java new file mode 100644 index 0000000..3281922 --- /dev/null +++ b/NotesDatabaseHelper.java @@ -0,0 +1,475 @@ +/* + * 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.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; + +/* + * NotesDatabaseHelper 类继承自 SQLiteOpenHelper,用于管理 SQLite 数据库的创建和版本升级。 + * 该类负责创建和管理两个主要的数据库表:NOTE 表和 DATA 表,以及相关的触发器和索引。 + */ +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 语句 + /* + * 创建笔记表的 SQL 语句。 + * 该表用于存储笔记的相关信息,包括笔记的 ID、父 ID、提醒日期、背景颜色 ID、创建日期、 + * 是否有附件、修改日期、笔记数量、摘要、类型、小部件 ID、小部件类型、同步 ID、本地修改标志、 + * 原始父 ID、Google 任务 ID 和版本号。 + */ + 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," + // 父笔记的 ID,默认为 0 + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 提醒日期,默认为 0 + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景颜色 ID,默认为 0 + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建日期,默认为当前时间 + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + // 是否有附件,默认为 0 + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改日期,默认为当前时间 + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 笔记数量,默认为 0 + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 笔记摘要,默认为空字符串 + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 笔记类型,默认为 0 + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + // 小部件 ID,默认为 0 + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 小部件类型,默认为 -1 + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步 ID,默认为 0 + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 本地修改标志,默认为 0 + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 原始父 ID,默认为 0 + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // Google 任务 ID,默认为空字符串 + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本号,默认为 0 + ")"; + // 创建 DATA 表的 SQL 语句 + /* + * 创建数据表的 SQL 语句。 + * 该表用于存储与笔记相关的数据,包括数据的 ID、MIME 类型、所属笔记的 ID、创建日期、 + * 修改日期、内容、以及一些额外的数据字段(DATA1 到 DATA5)。 + */ + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + // 数据的唯一标识符,主键 + DataColumns.MIME_TYPE + " TEXT NOT NULL," + // MIME 类型,不能为空 + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 所属笔记的 ID,默认为 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," + // 额外数据字段 1,整数类型 + DataColumns.DATA2 + " INTEGER," + // 额外数据字段 2,整数类型 + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 额外数据字段 3,文本类型,默认为空字符串 + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 额外数据字段 4,文本类型,默认为空字符串 + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 额外数据字段 5,文本类型,默认为空字符串 + ")"; + // 创建 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 + ");"; + // 当笔记移动到文件夹时,增加文件夹的笔记计数触发器 + /** + * Increase folder's note count when move note to the folder + */ + /* + * 创建一个触发器,用于在更新笔记的父 ID 时增加父文件夹的笔记数量。 + * 该触发器在笔记表的 PARENT_ID 字段更新后触发,并将父文件夹的 NOTES_COUNT 字段加 1。 + */ + 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"; + // 当笔记从文件夹中移出时,减少文件夹的笔记计数触发器 + /** + * Decrease folder's note count when move note from folder + */ + /* + * 创建一个触发器,用于在更新笔记的父 ID 时减少旧父文件夹的笔记数量。 + * 该触发器在笔记表的 PARENT_ID 字段更新后触发,并将旧父文件夹的 NOTES_COUNT 字段减 1。 + */ + 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"; + // 当插入新笔记到文件夹时,增加文件夹的笔记计数触发器 + /** + * Increase folder's note count when insert new note to the folder + */ + /* + * 创建一个触发器,用于在插入新笔记时增加父文件夹的笔记数量。 + * 该触发器在笔记表插入新记录后触发,并将父文件夹的 NOTES_COUNT 字段加 1。 + */ + 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"; + // 当从文件夹中删除笔记时,减少文件夹的笔记计数触发器 + /** + * Decrease folder's note count when delete note from the folder + */ + /* + * 创建一个触发器,用于在删除笔记时减少父文件夹的笔记数量。 + * 该触发器在笔记表删除记录后触发,并将父文件夹的 NOTES_COUNT 字段减 1。 + */ + 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"; + // 当插入类型为 {@link DataConstants#NOTE} 的数据时,更新笔记内容触发器 + /** + * Update note's content when insert data with type {@link DataConstants#NOTE} + */ + /* + * 创建一个触发器,用于在插入新数据时更新笔记的内容摘要。 + * 该触发器在数据表插入新记录后触发,并且仅在插入的数据 MIME 类型为 'NOTE' 时执行。 + * 触发器会将笔记表中对应笔记的 SNIPPET 字段更新为新插入数据的内容。 + */ + 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"; + // 当类型为 {@link DataConstants#NOTE} 的数据更新时,更新笔记内容触发器 + /** + * Update note's content when data with {@link DataConstants#NOTE} type has changed + */ + /* + * 创建一个触发器,用于在更新数据时更新笔记的内容摘要。 + * 该触发器在数据表更新记录后触发,并且仅在更新的数据 MIME 类型为 'NOTE' 时执行。 + * 触发器会将笔记表中对应笔记的 SNIPPET 字段更新为更新后的数据内容。 + */ + 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"; + // 当类型为 {@link DataConstants#NOTE} 的数据删除时,更新笔记内容触发器 + /** + * Update note's content when data with {@link DataConstants#NOTE} type has deleted + */ + /* + *创建一个触发器,用于在删除数据时更新笔记的内容摘要。 + * 该触发器在数据表删除记录后触发,并且仅在删除的数据 MIME 类型为 'NOTE' 时执行。 + * 触发器会将笔记表中对应笔记的 SNIPPET 字段清空。 + */ + 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"; + // 当删除笔记时,删除相关数据触发器 + /** + * Delete datas belong to note which has been deleted + */ + /* + * 创建一个触发器,用于在删除笔记时删除与之关联的数据。 + * 该触发器在笔记表删除记录后触发,并将数据表中与被删除笔记关联的所有数据删除。 + */ + 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"; + // 当删除文件夹时,删除文件夹中的笔记触发器 + /** + * Delete notes belong to folder which has been deleted + */ + /* + *创建一个触发器,用于在删除笔记时删除与之关联的数据。 + * 该触发器在笔记表删除记录后触发,并将数据表中与被删除笔记关联的所有数据删除。 + */ + 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"; + // 当文件夹移动到回收站时,移动文件夹中的笔记到回收站触发器 + /** + * Move notes belong to folder which has been moved to trash folder + */ + /* + * 创建一个触发器,用于在将文件夹移动到回收站时,将其所有子笔记也移动到回收站。 + * 该触发器在笔记表更新记录后触发,并且仅在更新的笔记的 PARENT_ID 字段值为回收站文件夹 ID 时执行。 + * 触发器会将所有父 ID 为被更新笔记 ID 的笔记的 PARENT_ID 字段值更新为回收站文件夹 ID。 + */ + 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"; +/* + * 构造函数,初始化数据库 + * 该构造函数用于初始化数据库帮助类,并指定数据库名称和版本号。 + * @param context 上下文 + */ + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } +/* + * 创建 NOTE 表 + * 创建笔记表及其相关触发器和系统文件夹。 + * 该方法用于在数据库中创建笔记表,并调用相关方法创建触发器和系统文件夹。 + * @param db SQLiteDatabase 实例 + */ + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); // 执行创建笔记表的 SQL 语句 + reCreateNoteTableTriggers(db); // 重新创建笔记表的触发器 + createSystemFolder(db); // 创建系统文件夹 + Log.d(TAG, "note table has been created"); // 记录日志,表示笔记表已创建 + } +/* + * 重新创建 NOTE 表的触发器 + * 该方法用于删除现有的笔记表触发器,并重新创建它们,以确保触发器与表结构一致。 + * @param 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); + } +/* + * 创建系统文件夹 + * 该方法用于在数据库中创建系统文件夹,包括通话记录文件夹、根文件夹、临时文件夹和回收站文件夹。 + * @param db SQLiteDatabase 实例 + */ + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + // 创建通话记录文件夹, 通话记录文件夹,用于存储通话笔记。 + /** + * call record foler for call notes + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建根文件夹(默认文件夹) + /** + * root folder which is default folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建临时文件夹(用于移动笔记) + /** + * temporary folder which is used for moving note + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 创建回收站文件夹 + /** + * create trash folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } +/* + * 创建 DATA 表 + * 该方法用于在数据库中创建数据表,并调用相关方法创建触发器和索引。 + * @param db SQLiteDatabase 实例 + */ + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); // 执行创建数据表的 SQL 语句 + reCreateDataTableTriggers(db); // 重新创建数据表的触发器 + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); // 创建数据表的索引 + Log.d(TAG, "data table has been created"); // 记录日志,表示数据表已创建 + } +/* + * 重新创建 DATA 表的触发器 + * 该方法用于删除现有的数据表触发器,并重新创建它们,以确保触发器与表结构一致。 + * @param 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 的单例实例 + * 该方法使用 synchronized 关键字确保线程安全,并在实例不存在时创建一个新的实例。 + * @param context 上下文 + * @return 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 旧版本号 + * @param newVersion 新版本号 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + // 如果旧版本是 1,执行从版本 1 升级到版本 2 的操作 + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // // 这个升级包括从 v2 到 v3 的升级 + oldVersion++; + } + // 如果旧版本是 2,并且没有跳过 v2 升级,执行从版本 2 升级到版本 3 的操作 + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + // 如果旧版本是 3,执行从版本 3 升级到版本 4 的操作 + 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"); + } + } +/* + * 升级数据库到版本 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); // 重新创建数据表及其相关触发器和索引 + } +/* + * 升级数据库到版本 3 + * @param db SQLiteDatabase 实例 + */ + private void upgradeToV3(SQLiteDatabase db) { + // drop unused triggers + // 删除不再使用的触发器 + 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"); + // add a column for gtask id + //为 gtask id 添加列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // add a trash system folder + // 添加回收站系统文件夹 + 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); + } +/* + *升级数据库到版本 4 + * @param db SQLiteDatabase 实例 + */ + private void upgradeToV4(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} diff --git a/NotesProvider.java b/NotesProvider.java new file mode 100644 index 0000000..0bff3b3 --- /dev/null +++ b/NotesProvider.java @@ -0,0 +1,375 @@ +/* + * 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.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; + +/* + * NotesProvider 是一个 ContentProvider,用于管理笔记数据的增删改查操作。 + * 它通过 UriMatcher 来匹配不同的 URI,并根据匹配结果执行相应的数据库操作。 + */ +public class NotesProvider extends ContentProvider { + private static final UriMatcher mMatcher; + // UriMatcher 用于匹配不同的 URI + 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; // 匹配搜索建议 + // URI 匹配码 + static { // 初始化 UriMatcher + 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); + } +/* + * 搜索结果的投影字段,用于构建搜索查询。 + *x'0A' 表示 SQLite 中的换行符 '\n'。 + * 在搜索结果中,标题和内容会去除换行符和空白字符,以便显示更多信息。 + */ + /** + * x'0A' represents the '\n' character in sqlite. For title and content in the search result, + * we will trim '\n' and white space in order to show more information. + */ + 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 查询语句。 + * 查询条件包括:笔记摘要包含搜索关键字、父 ID 不是回收站文件夹、笔记类型为普通笔记。 + */ + 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; // 返回 true 表示初始化成功 + } +/* + * 查询操作。 + * 该方法根据传入的 URI 执行相应的查询操作,并返回查询结果的 Cursor。 + * @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; + 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); // 获取 URI 中的笔记 ID + 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); // 获取 URI 中的数据 ID + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); // 查询单个数据项 + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); // 获取搜索建议的搜索字符串 + } + } else { + searchString = uri.getQueryParameter("pattern"); // 获取搜索的搜索字符串 + } + + 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); // 设置 Cursor 的通知 URI + } + return c; // 返回查询结果的 Cursor + } +/* + * 插入操作。 + * 该方法根据传入的 URI 执行相应的插入操作,并返回插入后的 URI。 + * @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); // 获取笔记 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 异常 + } + // Notify the note uri + // 通知笔记 URI 变化 + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // Notify the data uri + // 通知数据 URI 变化 + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + return ContentUris.withAppendedId(uri, insertedId); // 返回插入后的 URI + } +/* + * 删除操作。 + * 该方法根据传入的 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 "; // 添加删除条件,ID 大于 0 + count = db.delete(TABLE.NOTE, selection, selectionArgs); // 删除笔记表中的数据 + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); // 获取 URI 中的笔记 ID + /** + * ID that smaller than 0 is system folder which is not allowed to + * trash + */ + /* + * 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); // 获取 URI 中的数据 ID + 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); // 通知笔记 URI 变化 + } + getContext().getContentResolver().notifyChange(uri, null); // 通知 URI 变化 + } + return count; // 返回删除的行数 + } +/* + * 更新操作。 + * 该方法根据传入的 URI 执行相应的更新操作,并返回更新的行数。 + * @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); // 获取 URI 中的笔记 ID + 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); // 获取 URI 中的数据 ID + 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); // 通知笔记 URI 变化 + } + getContext().getContentResolver().notifyChange(uri, null); // 通知 URI 变化 + } + return count; // 返回更新的行数 + } +/* + * 解析选择条件,用于在查询或更新时附加额外的条件。 + * @param selection 原始选择条件。 + * @return 返回附加了额外条件的字符串。 + */ + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } +/* + * 增加笔记版本号。 + *该方法用于增加指定笔记的版本号,或者根据选择条件增加符合条件的笔记的版本号。 + * @param id 笔记 ID,如果为 -1 则表示更新所有符合条件的笔记。 + * @param selection 更新条件。 + * @param selectionArgs 更新条件的参数。 + */ + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); // 创建 StringBuilder 对象,用于构建 SQL 语句 + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); // 增加版本号 + + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); // 根据笔记 ID 更新 + } + if (!TextUtils.isEmpty(selection)) { + String selectString = id > 0 ? parseSelection(selection) : selection; + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); // 替换选择条件中的占位符 + } + sql.append(selectString); + } + + mHelper.getWritableDatabase().execSQL(sql.toString()); // 执行 SQL 语句 + } +/* + * 获取 MIME 类型。 + * @param uri 查询的 URI。 + * @return 返回 MIME 类型。 + */ + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +} -- 2.34.1 From 88d5b2849d051d4518a69f30c0fd848602e6b6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:18:51 +0800 Subject: [PATCH 04/44] Signed-off-by: --- MetaData.java | 115 ++++++++++ Node.java | 158 +++++++++++++ SqlData.java | 243 ++++++++++++++++++++ SqlNote.java | 618 ++++++++++++++++++++++++++++++++++++++++++++++++++ Task.java | 454 ++++++++++++++++++++++++++++++++++++ TaskList.java | 465 +++++++++++++++++++++++++++++++++++++ 6 files changed, 2053 insertions(+) create mode 100644 MetaData.java create mode 100644 Node.java create mode 100644 SqlData.java create mode 100644 SqlNote.java create mode 100644 Task.java create mode 100644 TaskList.java diff --git a/MetaData.java b/MetaData.java new file mode 100644 index 0000000..1f73277 --- /dev/null +++ b/MetaData.java @@ -0,0 +1,115 @@ +/* + * 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.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +/* + *MetaData 类继承自 Task 类,用于处理与 Google Task 相关的元数据。 + * 该类主要用于存储和获取与 Google Task 相关的元数据信息。 + */ +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); + + private String mRelatedGid = null; // 相关 Google Task 的 ID +/* + * 设置元数据信息。 + * 该方法用于将 Google Task 的 ID 和元数据信息存储到任务的备注字段中。 + * @param gid Google Task 的 ID。 + * @param metaInfo 元数据信息的 JSON 对象。 + */ + public void setMeta(String gid, JSONObject metaInfo) { + try { + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); // 将 Google Task 的 ID 添加到元数据信息中 + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); // 记录日志,添加失败 + } + setNotes(metaInfo.toString()); // 将元数据信息存储到任务的备注字段中 + setName(GTaskStringUtils.META_NOTE_NAME); // 设置任务的名称为元数据名称 + } +/* + * 获取相关 Google Task 的 ID。 + * @return 返回相关 Google Task 的 ID。 + */ + public String getRelatedGid() { + return mRelatedGid; + } +/* + * 判断任务是否值得保存。 + * 该方法用于判断任务是否包含有效的备注信息。 + * @return 如果任务包含有效的备注信息,则返回 true,否则返回 false。 + */ + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } +/* + * 根据远程 JSON 对象设置任务内容。 + * 该方法用于从远程 JSON 对象中提取元数据信息,并设置相关 Google Task 的 ID。 + * @param js 远程 JSON 对象。 + */ + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); // 调用父类方法设置任务内容 + if (getNotes() != null) { + try { + JSONObject metaInfo = new JSONObject(getNotes().trim()); // 解析备注字段中的元数据信息 + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); // 获取相关 Google Task 的 ID + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); // 记录日志,获取失败 + mRelatedGid = null; + } + } + } +/* + * 根据本地 JSON 对象设置任务内容。 + * 该方法不应被调用,因为元数据任务不应从本地 JSON 对象中设置内容。 + * @param js 本地 JSON 对象。 + */ + @Override + public void setContentByLocalJSON(JSONObject js) { + // this function should not be called + // 该方法不应被调用 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } +/* + * 从任务内容中获取本地 JSON 对象。 + * 该方法不应被调用,因为元数据任务不应生成本地 JSON 对象。 + * @return 抛出异常,表示该方法不应被调用。 + */ + @Override + public JSONObject getLocalJSONFromContent() { + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } +/* + * 获取同步操作。 + * 该方法不应被调用,因为元数据任务不应进行同步操作。 + * @param c 数据库游标。 + * @return 抛出异常,表示该方法不应被调用。 + */ + @Override + public int getSyncAction(Cursor c) { + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } + +} diff --git a/Node.java b/Node.java new file mode 100644 index 0000000..670361c --- /dev/null +++ b/Node.java @@ -0,0 +1,158 @@ +/* + * 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 org.json.JSONObject; +/* + * Node 类是一个抽象类,用于表示 Google Task 中的节点。 + * 该类定义了节点的基本属性和操作,并提供了抽象方法供子类实现。 + */ +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; // Google Task 的 ID + + private String mName; // 节点名称 + + private long mLastModified; // 最后修改时间 + + private boolean mDeleted; // 是否已删除 +/* + * 构造函数,初始化节点属性。 + */ + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } +/* + * 获取创建操作的 JSON 对象。 + * 该方法用于生成创建操作的 JSON 对象。 + * @param actionId 操作 ID。 + * @return 返回创建操作的 JSON 对象。 + */ + public abstract JSONObject getCreateAction(int actionId); +/* + * 获取更新操作的 JSON 对象。 + * 该方法用于生成更新操作的 JSON 对象。 + * @param actionId 操作 ID。 + * @return 返回更新操作的 JSON 对象。 + */ + public abstract JSONObject getUpdateAction(int actionId); +/* + * 根据远程 JSON 对象设置节点内容。 + * 该方法用于从远程 JSON 对象中提取节点内容。 + * @param js 远程 JSON 对象。 + */ + public abstract void setContentByRemoteJSON(JSONObject js); +/* + * 根据本地 JSON 对象设置节点内容。 + * 该方法用于从本地 JSON 对象中提取节点内容。 + * @param js 本地 JSON 对象。 + */ + public abstract void setContentByLocalJSON(JSONObject js); +/* + *从节点内容中获取本地 JSON 对象。 + * 该方法用于将节点内容转换为本地 JSON 对象。 + * @return 返回本地 JSON 对象。 + */ + public abstract JSONObject getLocalJSONFromContent(); +/* + * 获取同步操作。 + * 该方法用于根据数据库游标获取同步操作。 + * @param c 数据库游标。 + * @return 返回同步操作。 + */ + public abstract int getSyncAction(Cursor c); +/* + * 设置 Google Task 的 ID。 + * @param gid Google Task 的 ID。 + */ + public void setGid(String gid) { + this.mGid = gid; + } +/* + * 设置节点名称。 + * @param name 节点名称。 + */ + public void setName(String name) { + this.mName = name; + } +/* + * 设置最后修改时间。 + * @param lastModified 最后修改时间。 + */ + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } +/* + * 设置节点是否已删除。 + * @param deleted 是否已删除。 + */ + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } +/* + * 获取 Google Task 的 ID + * @return 返回 Google Task 的 ID。 + */ + public String getGid() { + return this.mGid; + } +/* + * 获取节点名称。 + * @return 返回节点名称。 + */ + public String getName() { + return this.mName; + } +/* + * 获取最后修改时间。 + * @return 返回最后修改时间。 + */ + public long getLastModified() { + return this.mLastModified; + } +/* + * 获取节点是否已删除。 + * @return 返回节点是否已删除。 + */ + public boolean getDeleted() { + return this.mDeleted; + } + +} diff --git a/SqlData.java b/SqlData.java new file mode 100644 index 0000000..4cdeb85 --- /dev/null +++ b/SqlData.java @@ -0,0 +1,243 @@ +/* + * 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.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; + +/* + * SqlData 类用于处理与数据库相关的数据操作。 + * 该类主要用于加载、设置、获取和提交数据。 + */ +public class SqlData { + private static final String TAG = SqlData.class.getSimpleName(); + + private static final int INVALID_ID = -99999; // 无效的 ID + // 数据表的投影字段 + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + // 数据表的列索引 + 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; + + private ContentResolver mContentResolver; // 内容解析器 + + private boolean mIsCreate; // 是否为创建操作 + + private long mDataId; // 数据 ID + + private String mDataMimeType; // 数据 MIME 类型 + + private String mDataContent; // 数据内容 + + private long mDataContentData1; // 数据内容数据 1 + + private String mDataContentData3; // 数据内容数据 3 + + private ContentValues mDiffDataValues; // 差异数据值 +/* + * 构造函数,用于创建新的数据对象。 + * 该构造函数初始化数据对象的各个属性,并设置为创建操作。 + * @param context 应用程序上下文。 + */ + 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(); // 初始化差异数据值 + } +/* + * 构造函数,用于从数据库游标加载数据对象。 + * 该构造函数初始化数据对象的各个属性,并设置为非创建操作。 + * @param context 应用程序上下文。 + * @param c 数据库游标。 + */ + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = false; // 设置为非创建操作 + loadFromCursor(c); // 从数据库游标加载数据 + mDiffDataValues = new ContentValues(); // 初始化差异数据值 + } +/* + * 从数据库游标加载数据。 + * 该方法从数据库游标中提取数据,并初始化数据对象的各个属性。 + * @param c 数据库游标。 + */ + 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 对象中提取数据,并更新数据对象的各个属性。 + * 如果数据对象是新创建的,或者 JSON 对象中的数据与当前数据对象的数据不同,则更新差异数据值。 + * @param js JSON 对象,包含数据内容。 + * @throws JSONException 如果解析 JSON 对象时发生异常。 + */ + public void setContent(JSONObject js) throws JSONException { + // 从 JSON 对象中获取数据 ID,如果 JSON 对象中没有数据 ID,则使用无效 ID + long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; + // 如果数据对象是新创建的,或者数据 ID 不同,则更新差异数据值 + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); + } + mDataId = dataId; // 更新数据 ID + // 从 JSON 对象中获取数据 MIME 类型,如果 JSON 对象中没有数据 MIME 类型,则使用默认的笔记类型 + 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); + } + mDataMimeType = dataMimeType; // 更新数据 MIME 类型 + // 从 JSON 对象中获取数据内容,如果 JSON 对象中没有数据内容,则使用空字符串 + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + // 如果数据对象是新创建的,或者数据内容不同,则更新差异数据值 + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; // 更新数据内容 + // 从 JSON 对象中获取数据内容数据 1,如果 JSON 对象中没有数据内容数据 1,则使用 0 + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + // 如果数据对象是新创建的,或者数据内容数据 1 不同,则更新差异数据值 + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; // 更新数据内容数据 1 + // 从 JSON 对象中获取数据内容数据 3,如果 JSON 对象中没有数据内容数据 3,则使用空字符串 + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + // 如果数据对象是新创建的,或者数据内容数据 3 不同,则更新差异数据值 + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; // 更新数据内容数据 3 + } +/* + * 获取数据内容。 + * 该方法将数据对象的各个属性转换为 JSON 对象,并返回该 JSON 对象。 + * @return 返回包含数据内容的 JSON 对象。 + * @throws JSONException 如果生成 JSON 对象时发生异常。 + */ + public JSONObject getContent() throws JSONException { + // 如果数据对象是新创建的,则记录错误日志并返回 null + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + JSONObject js = new JSONObject(); // 创建一个新的 JSON 对象 + js.put(DataColumns.ID, mDataId); // 将数据 ID 添加到 JSON 对象中 + js.put(DataColumns.MIME_TYPE, mDataMimeType); // 将数据 MIME 类型添加到 JSON 对象中 + js.put(DataColumns.CONTENT, mDataContent); // 将数据内容添加到 JSON 对象中 + js.put(DataColumns.DATA1, mDataContentData1); // 将数据内容数据 1 添加到 JSON 对象中 + js.put(DataColumns.DATA3, mDataContentData3); // 将数据内容数据 3 添加到 JSON 对象中 + return js; // 返回 JSON 对象 + } +/* + * 提交数据。 + * 该方法根据数据对象的状态(创建或更新)执行相应的数据库操作。 + * 如果是创建操作,则插入数据到数据库中;如果是更新操作,则更新数据库中的数据。 + * @param noteId 笔记 ID。 + * @param validateVersion 是否验证版本。 + * @param version 版本号。 + */ + public void commit(long noteId, boolean validateVersion, long version) { + // 如果是创建操作 + if (mIsCreate) { + // 如果数据 ID 是无效的,并且差异数据值中包含数据 ID,则移除数据 ID + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + // 将笔记 ID 添加到差异数据值中 + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + // 插入数据到数据库中 + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); + try { + // 获取插入数据后的数据 ID + mDataId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + // 记录错误日志,并抛出操作失败异常 + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + } 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。 + * 该方法返回数据对象的数据 ID。 + * @return 返回数据 ID。 + */ + public long getId() { + return mDataId; + } +} diff --git a/SqlNote.java b/SqlNote.java new file mode 100644 index 0000000..0283258 --- /dev/null +++ b/SqlNote.java @@ -0,0 +1,618 @@ +/* + * 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(); + + private static final int INVALID_ID = -99999; // 无效的 ID + // 笔记表的投影字段 + 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; // 数据列表 +/* + * 构造函数,用于创建新的笔记对象。 + * 该构造函数初始化笔记对象的各个属性,并设置为创建操作。 + * @param context 应用程序上下文。 + */ + public SqlNote(Context context) { + mContext = context; // 设置应用程序上下文 + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = true; // 设置为创建操作 + mId = INVALID_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 为无效值 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 初始化小部件类型为无效值 + mOriginParent = 0; // 初始化原始父笔记 ID 为 0 + mVersion = 0; // 初始化版本号为 0 + mDiffNoteValues = new ContentValues(); // 初始化差异笔记值 + mDataList = new ArrayList(); // 初始化数据列表 + } +/* + * 构造函数,用于从数据库游标加载笔记对象。 + * 该构造函数初始化笔记对象的各个属性,并设置为非创建操作。 + * @param context 应用程序上下文。 + * @param c 数据库游标。 + */ + public SqlNote(Context context, Cursor c) { + mContext = context; // 设置应用程序上下文 + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = false; // 设置为非创建操作 + loadFromCursor(c); // 从数据库游标加载笔记对象 + mDataList = new ArrayList(); // 初始化数据列表 + if (mType == Notes.TYPE_NOTE) // 如果笔记类型为普通笔记 + loadDataContent(); // 加载笔记的数据内容 + mDiffNoteValues = new ContentValues(); // 初始化差异笔记值 + } +/* + * 构造函数,用于从数据库中加载指定 ID 的笔记对象。 + * 该构造函数初始化笔记对象的各个属性,并设置为非创建操作。 + * @param context 应用程序上下文。 + * @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(); // 初始化差异笔记值 + + } +/* + * 从数据库中加载指定 ID 的笔记对象。 + * 该方法从数据库中查询指定 ID 的笔记数据,并调用 `loadFromCursor(Cursor c)` 方法加载笔记对象。 + * @param id 笔记 ID。 + */ + 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) { + c.moveToNext(); // 移动到游标的第一行 + loadFromCursor(c); // 加载笔记对象 + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); // 记录警告日志,游标为空 + } + } finally { + if (c != null) + c.close(); // 关闭游标 + } + } +/* + * 从数据库游标加载笔记对象。 + * 该方法从数据库游标中提取笔记数据,并初始化笔记对象的各个属性。 + * @param c 数据库游标。 + */ + 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); // 获取版本号 + } +/* + * 加载笔记的数据内容。 + * 该方法从数据库中查询与笔记相关的数据内容,并将其加载到数据列表中。 + */ + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); // 清空数据列表 + try { + // 查询与笔记相关的数据内容 + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c != null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); // 记录警告日志,笔记没有数据内容 + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); // 从游标中加载数据 + mDataList.add(data); // 将数据内容添加到数据列表中 + } + } else { + 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 { + // 从JSONObject中获取笔记的元数据 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 检查笔记的类型 + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 如果是系统文件夹,则不能设置内容,记录警告日志 + Log.w(TAG, "cannot set system folder"); + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 如果是文件夹类型,则只能更新摘要和类型 + // for folder we can only update the snnipet and type + // 获取摘要,如果未提供则默认为空字符串 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + // 如果是新建笔记或摘要有变化,则更新摘要 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + // 获取类型,如果未提供则默认为普通笔记类型 + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + // 如果是新建笔记或类型有变化,则更新类型 + 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); + // 获取笔记ID,如果未提供则默认为无效ID + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + // 如果是新建笔记或ID有变化,则更新ID + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); + } + mId = id; + // 获取提醒日期,如果未提供则默认为0 + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note + .getLong(NoteColumns.ALERTED_DATE) : 0; + // 如果是新建笔记或提醒日期有变化,则更新提醒日期 + if (mIsCreate || mAlertDate != alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); + } + mAlertDate = alertDate; + // 获取背景颜色ID,如果未提供则默认为默认背景颜色ID + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note + .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + // 如果是新建笔记或背景颜色ID有变化,则更新背景颜色ID + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + mBgColorId = bgColorId; + // 获取创建日期,如果未提供则默认为当前时间 + long createDate = note.has(NoteColumns.CREATED_DATE) ? note + .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + // 如果是新建笔记或创建日期有变化,则更新创建日期 + if (mIsCreate || mCreatedDate != createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); + } + mCreatedDate = createDate; + // 获取是否有附件,如果未提供则默认为0(无附件) + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note + .getInt(NoteColumns.HAS_ATTACHMENT) : 0; + // 如果是新建笔记或是否有附件有变化,则更新是否有附件 + if (mIsCreate || mHasAttachment != hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); + } + mHasAttachment = hasAttachment; + // 获取修改日期,如果未提供则默认为当前时间 + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + // 如果是新建笔记或修改日期有变化,则更新修改日期 + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } + mModifiedDate = modifiedDate; + // 获取父ID,如果未提供则默认为0 + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; + // 如果是新建笔记或父ID有变化,则更新父ID + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } + mParentId = parentId; + // 获取摘要,如果未提供则默认为空字符串 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + // 如果是新建笔记或摘要有变化,则更新摘要 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + // 获取类型,如果未提供则默认为普通笔记类型 + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + // 如果是新建笔记或类型有变化,则更新类型 + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + // 获取小部件ID,如果未提供则默认为无效小部件ID + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + // 如果是新建笔记或小部件ID有变化,则更新小部件ID + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } + mWidgetId = widgetId; + // 获取小部件类型,如果未提供则默认为无效小部件类型 + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + // 如果是新建笔记或小部件类型有变化,则更新小部件类型 + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } + mWidgetType = widgetType; + // 获取原始父ID,如果未提供则默认为0 + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + // 如果是新建笔记或原始父ID有变化,则更新原始父ID + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } + mOriginParent = originParent; + // 遍历数据数组,更新数据列表 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + // 如果数据对象包含ID,则在现有数据列表中查找匹配的SqlData对象 + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + // 如果未找到匹配的SqlData对象,则创建一个新的SqlData对象并添加到数据列表中 + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); + } + // 设置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用于存储内容 + JSONObject js = new JSONObject(); + // 如果当前对象尚未在数据库中创建,记录错误日志并返回null + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + // 创建一个新的JSONObject用于存储笔记信息 + JSONObject note = new JSONObject(); + // 如果当前对象是普通笔记类型 + if (mType == Notes.TYPE_NOTE) { + // 将笔记的各个属性添加到note JSONObject中 + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); + note.put(NoteColumns.CREATED_DATE, mCreatedDate); + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); + note.put(NoteColumns.PARENT_ID, mParentId); + note.put(NoteColumns.SNIPPET, mSnippet); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.WIDGET_ID, mWidgetId); + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + // 将note JSONObject添加到js JSONObject中 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + // 创建一个新的JSONArray用于存储数据列表 + JSONArray dataArray = new JSONArray(); + // 遍历数据列表,将每个SqlData对象的内容添加到dataArray中 + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); + if (data != null) { + dataArray.put(data); + } + } + // 将dataArray添加到js JSONObject中 + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + // 如果当前对象是文件夹或系统文件夹类型 + // 将笔记的ID、类型和摘要添加到note JSONObject中 + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.SNIPPET, mSnippet); + // 将note JSONObject添加到js JSONObject中 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + } + // 返回包含内容的JSONObject + return js; + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; + } +/* + * 设置父ID,并将其添加到差异值中。 + * @param id 新的父ID + */ + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } +/* + * 设置Google任务ID,并将其添加到差异值中。 + * @param gid 新的Google任务ID + */ + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } +/* + * 设置同步ID,并将其添加到差异值中。 + * @param syncId 新的同步ID + */ + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } +/* + * 重置本地修改标志,将其设置为0。 + */ + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } +/* + * 获取当前笔记的ID。 + * @return 当前笔记的ID + */ + public long getId() { + return mId; + } +/* + * 获取当前笔记的父ID。 + * @return 当前笔记的父ID + */ + public long getParentId() { + return mParentId; + } +/* + * 获取当前笔记的摘要。 + * @return 当前笔记的摘要 + */ + public String getSnippet() { + return mSnippet; + } +/* + * 判断当前笔记是否为普通笔记类型。 + * @return 如果当前笔记是普通笔记类型,返回true;否则返回false + */ + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } +/* + * 提交更改,将当前对象的差异值保存到数据库中。 + * @param validateVersion 是否验证版本号 + */ + public void commit(boolean validateVersion) { + // 如果是新建笔记 + if (mIsCreate) { + // 如果ID无效且差异值中包含ID,则移除ID + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } + // 插入笔记到数据库,并获取插入后的URI + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + // 从URI中提取新插入笔记的ID + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + // 如果提取ID失败,记录错误日志并抛出异常 + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + // 如果ID为0,表示插入失败,抛出异常 + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + // 如果当前笔记是普通笔记类型 + if (mType == Notes.TYPE_NOTE) { + // 遍历数据列表,提交每个SqlData对象的更改 + 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) + }); + } else { + // 如果验证版本号,更新笔记内容并检查版本号 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + // 如果没有更新内容,记录警告日志 + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + // 如果当前笔记是普通笔记类型 + if (mType == Notes.TYPE_NOTE) { + // 遍历数据列表,提交每个SqlData对象的更改 + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); + } + } + } + + // refresh local info + // 刷新本地信息 + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + // 清空差异值并设置为非新建状态 + mDiffNoteValues.clear(); + mIsCreate = false; + } +} diff --git a/Task.java b/Task.java new file mode 100644 index 0000000..8314242 --- /dev/null +++ b/Task.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/* + * Task类表示一个任务,继承自Node类。 + * 该类包含了任务的各种属性和操作方法,如创建、更新、设置内容等。 + */ +public class Task extends Node { + private static final String TAG = Task.class.getSimpleName(); + + private boolean mCompleted; // 任务是否已完成 + + private String mNotes; // 任务的备注 + + private JSONObject mMetaInfo; // 任务的元数据信息 + + private Task mPriorSibling; // 前一个兄弟任务 + + private TaskList mParent; // 父任务列表 +/* + * 构造函数,初始化任务对象。 + * 初始化任务的完成状态、备注、前一个兄弟任务、父任务列表和元数据信息。 + */ + public Task() { + super(); // 调用父类Node的构造函数 + mCompleted = false; // 初始化任务为未完成状态 + mNotes = null; // 初始化备注为空 + mPriorSibling = null; // 初始化前一个兄弟任务为空 + mParent = null; // 初始化父任务列表为空 + mMetaInfo = null; // 初始化元数据信息为空 + } +/* + *生成创建任务的JSON对象。 + * @param actionId 操作ID + * @return 包含创建任务信息的JSONObject + */ + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 设置操作类型为创建 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + // 设置操作ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + // 设置任务在父任务列表中的索引 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // entity_delta + // 设置任务的实体信息 + 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); + + // parent_id + // 设置父任务列表的ID + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // dest_parent_type + // 设置目标父类型 + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // list_id + // 设置任务列表的ID + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // prior_sibling_id + // 设置前一个兄弟任务的ID + if (mPriorSibling != null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 抛出自定义异常,表示生成JSON对象失败 + throw new ActionFailureException("fail to generate task-create jsonobject"); + } + + return js; // 返回包含创建任务信息的JSONObject + } +/* + * 生成更新任务的JSON对象。 + * @param actionId 操作ID + * @return 包含更新任务信息的JSONObject + */ + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 设置操作类型为更新 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + // 设置操作ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + // 设置任务的ID + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + // 设置任务的实体信息 + 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); + + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 抛出自定义异常,表示生成JSON对象失败 + throw new ActionFailureException("fail to generate task-update jsonobject"); + } + + return js; // 返回包含更新任务信息的JSONObject + } +/* + * 根据远程JSON对象设置任务的内容。 + * @param js 包含任务信息的远程JSON对象 + */ + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + // 如果JSON对象中包含ID字段,则设置任务的ID + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + // 如果JSON对象中包含最后修改时间字段,则设置任务的最后修改时间 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + // 如果JSON对象中包含名称字段,则设置任务的名称 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + // notes + // 如果JSON对象中包含备注字段,则设置任务的备注 + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + + // deleted + // 如果JSON对象中包含删除状态字段,则设置任务的删除状态 + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + + // completed + // 如果JSON对象中包含完成状态字段,则设置任务的完成状态 + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 抛出自定义异常,表示从JSON对象获取任务内容失败 + throw new ActionFailureException("fail to get task content from jsonobject"); + } + } + } +/* + * 根据本地JSON对象设置任务的内容。 + * @param js 包含任务信息的本地JSON对象 + */ + public void setContentByLocalJSON(JSONObject js) { + // 检查JSON对象是否为空或是否包含必要的字段 + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) + || !js.has(GTaskStringUtils.META_HEAD_DATA)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + // 获取笔记的元数据 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 获取数据数组 + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 检查笔记的类型是否为普通笔记类型 + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + Log.e(TAG, "invalid type"); + return; + } + // 遍历数据数组,查找MIME类型为DataConstants.NOTE的数据 + 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) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } +/* + * 根据任务内容生成本地JSON对象。 + * @return 包含任务内容的本地JSON对象,如果发生错误则返回null + */ + public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + if (mMetaInfo == null) { + // new task created from web + // 如果是从网页创建的新任务 + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; + } + // 创建一个新的JSON对象 + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + data.put(DataColumns.CONTENT, name); + dataArray.put(data); + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + // synced task + // 如果是已同步的任务 + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 遍历数据数组,更新任务名称 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + data.put(DataColumns.CONTENT, getName()); + break; + } + } + + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; + } + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } +/* + * 设置任务的元数据信息。 + * @param metaData 包含元数据信息的MetaData对象 + */ + public void setMetaInfo(MetaData metaData) { + // 检查MetaData对象是否为空,并且是否包含笔记信息 + if (metaData != null && metaData.getNotes() != null) { + try { + // 将笔记信息转换为JSONObject对象 + mMetaInfo = new JSONObject(metaData.getNotes()); + } catch (JSONException e) { + // 捕获JSON异常并记录警告日志 + Log.w(TAG, e.toString()); + mMetaInfo = null; + // 如果转换失败,将mMetaInfo设置为null + } + } + } +/* + * 获取同步操作类型。 + * @param c 包含本地笔记信息的Cursor对象 + * @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字段,表示远程笔记ID已被删除 + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + // validate the note id now + // 验证笔记ID是否匹配 + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + // 如果本地没有修改 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + // 如果没有更新,返回SYNC_ACTION_NONE + return SYNC_ACTION_NONE; + } else { + // apply remote to local + // 应用远程更新到本地 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // validate gtask id + // 验证Google任务ID是否匹配 + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + // 如果同步ID与最后修改时间匹配,表示只有本地修改 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + 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; // 默认返回SYNC_ACTION_ERROR + } +/* + * 判断任务是否值得保存。 + * @return 如果任务值得保存,返回true;否则返回false + */ + public boolean isWorthSaving() { + // 检查元数据信息是否存在,或者任务名称和备注是否不为空且不为空白字符串 + return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) + || (getNotes() != null && getNotes().trim().length() > 0); + } +/* + * 设置任务的完成状态。 + * @param completed 任务的完成状态,true表示已完成,false表示未完成 + */ + public void setCompleted(boolean completed) { + this.mCompleted = completed; + } +/* + * 设置任务的备注。 + * @param notes 任务的备注 + */ + public void setNotes(String notes) { + this.mNotes = notes; + } +/* + * 设置前一个兄弟任务。 + * @param priorSibling 前一个兄弟任务 + */ + public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; + } +/* + * 设置父任务列表。 + * @param parent 父任务列表 + */ + public void setParent(TaskList parent) { + this.mParent = parent; + } +/* + * 获取任务的完成状态。 + * @return 任务的完成状态,true表示已完成,false表示未完成 + */ + public boolean getCompleted() { + return this.mCompleted; + } +/* + * 获取任务的备注。 + * @return 任务的备注 + */ + public String getNotes() { + return this.mNotes; + } +/* + * 获取前一个兄弟任务。 + * @return 前一个兄弟任务 + */ + public Task getPriorSibling() { + return this.mPriorSibling; + } +/* + * 获取父任务列表。 + * @return 父任务列表 + */ + public TaskList getParent() { + return this.mParent; + } + +} diff --git a/TaskList.java b/TaskList.java new file mode 100644 index 0000000..4c6b5bd --- /dev/null +++ b/TaskList.java @@ -0,0 +1,465 @@ +/* + * 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; + +/* + * TaskList类表示一个任务列表,继承自Node类。 + * 该类包含了任务列表的各种属性和操作方法,如创建、更新、设置内容等。 + */ +public class TaskList extends Node { + private static final String TAG = TaskList.class.getSimpleName(); + + private int mIndex; // 任务列表的索引 + + private ArrayList mChildren; // 子任务列表 +/* + * 构造函数,初始化任务列表对象。 + */ + public TaskList() { + super(); + mChildren = new ArrayList(); + mIndex = 1; + } +/* + * 生成创建任务列表的JSON对象。 + * @param actionId 操作ID + * @return 包含创建任务列表信息的JSONObject + */ + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 设置操作类型为创建 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + // 设置操作ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + // 设置索引 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // entity_delta + // 设置实体信息 + 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); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); + } + + return js; + } +/* + * 生成更新任务列表的JSON对象。 + * @param actionId 操作ID + * @return 包含更新任务列表信息的JSONObject + */ + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 设置操作类型为更新 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + // 设置操作ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + // 设置任务列表的ID + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + // 设置任务列表的实体信息 + 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); + + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 抛出自定义异常,表示生成JSON对象失败 + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); + } + + return js; // 返回包含更新任务列表信息的JSONObject + } +/* + * 根据远程JSON对象设置任务列表的内容。 + * @param js 包含任务列表信息的远程JSON对象 + */ + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + // 如果JSON对象中包含ID字段,则设置任务列表的ID + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + // 如果JSON对象中包含最后修改时间字段,则设置任务列表的最后修改时间 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + // 如果JSON对象中包含名称字段,则设置任务列表的名称 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 抛出自定义异常,表示从JSON对象获取任务列表内容失败 + throw new ActionFailureException("fail to get tasklist content from jsonobject"); + } + } + } +/* + * 根据本地JSON对象设置任务列表的内容。 + * @param js 包含任务列表信息的本地JSON对象 + */ + public void setContentByLocalJSON(JSONObject js) { + // 检查JSON对象是否为空或是否包含必要的字段 + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + 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) { + // 如果是系统文件夹类型,根据文件夹ID设置任务列表名称 + 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) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } +/* + * 根据任务列表内容生成本地JSON对象。 + * @return 包含任务列表内容的本地JSON对象,如果发生错误则返回null + */ + public JSONObject getLocalJSONFromContent() { + try { + // 创建一个新的JSON对象 + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); + // 获取任务列表的名称 + String folderName = getName(); + // 如果名称以GTaskStringUtils.MIUI_FOLDER_PREFFIX开头,则去掉前缀 + 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); + // 将文件夹信息添加到JSON对象中 + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + // 返回包含任务列表内容的本地JSON对象 + return js; + } catch (JSONException e) { + // 捕获JSON异常并记录错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 如果发生错误,返回null + return null; + } + } +/* + * 获取同步操作类型。 + * @param c 包含本地笔记信息的Cursor对象 + * @return 同步操作类型 + */ + public int getSyncAction(Cursor c) { + try { + // 如果本地没有修改 + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + // 如果没有更新,返回SYNC_ACTION_NONE + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + return SYNC_ACTION_NONE; + } else { + // 应用远程更新到本地 + // apply remote to local + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // 验证Google任务ID是否匹配 + // validate gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + // 如果同步ID与最后修改时间匹配,表示只有本地修改 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // for folder conflicts, just apply local modification + // 对于文件夹冲突,直接应用本地修改 + return SYNC_ACTION_UPDATE_REMOTE; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + // 捕获异常并记录错误日志 + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; // 默认返回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) { + // need to set prior sibling and parent + // 需要设置前一个兄弟任务和父任务 + 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); + + // update the task list + // 更新任务列表 + Task preTask = null; + Task afterTask = null; + if (index != 0) + preTask = mChildren.get(index - 1); + if (index != mChildren.size() - 1) + afterTask = mChildren.get(index + 1); + // 设置任务的前一个兄弟任务 + task.setPriorSibling(preTask); + // 如果存在后一个兄弟任务,则更新其后一个兄弟任务的前一个兄弟任务 + if (afterTask != null) + afterTask.setPriorSibling(task); + } + + return 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) { + // reset prior sibling and parent + // 重置任务的前一个兄弟任务和父任务 + task.setPriorSibling(null); + task.setParent(null); + + // update the task list + // 更新任务列表 + if (index != mChildren.size()) { + // 如果移除的任务不是最后一个任务,则更新其后一个任务的前一个兄弟任务 + mChildren.get(index).setPriorSibling( + index == 0 ? null : mChildren.get(index - 1)); + } + } + } + return ret; + } +/* + * 将子任务移动到指定索引位置。 + * @param task 要移动的子任务 + * @param index 目标索引位置 + * @return 如果成功移动子任务,返回true;否则返回false + */ + public boolean 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) { + // 遍历子任务列表 + for (int i = 0; i < mChildren.size(); i++) { + Task t = mChildren.get(i); + // 如果任务的GID与传入的GID匹配,则返回该任务 + if (t.getGid().equals(gid)) { + return t; + } + } + // 如果未找到匹配的任务,返回null + return 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 getChilTaskByGid(String gid) { + for (Task task : mChildren) { + if (task.getGid().equals(gid)) + return task; + } + return null; + } +/* + * 获取子任务列表。 + * @return 子任务列表 + */ + public ArrayList getChildTaskList() { + return this.mChildren; + } +/* + * 设置任务列表的索引。 + * @param index 任务列表的索引 + */ + public void setIndex(int index) { + this.mIndex = index; + } +/* + * 获取任务列表的索引。 + * @return 任务列表的索引 + */ + public int getIndex() { + return this.mIndex; + } +} -- 2.34.1 From a95ac1b67d506196b109813456e2830695a64a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:19:52 +0800 Subject: [PATCH 05/44] Signed-off-by: --- ActionFailureException.java | 45 ++++++++++++++++++++++++++++++++++++ NetworkFailureException.java | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 ActionFailureException.java create mode 100644 NetworkFailureException.java diff --git a/ActionFailureException.java b/ActionFailureException.java new file mode 100644 index 0000000..74811eb --- /dev/null +++ b/ActionFailureException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * + */ +package net.micode.notes.gtask.exception; +/* + * 自定义异常类,表示操作失败。 + * 继承自RuntimeException,用于在操作失败时抛出异常。 + */ +public class ActionFailureException extends RuntimeException { + private static final long serialVersionUID = 4425249765923293627L; + + public ActionFailureException() { //默认构造函数。 + super(); + } +/* + * 带错误信息的构造函数。 + * @param paramString 错误信息 + */ + public ActionFailureException(String paramString) { + super(paramString); + } +/* + * 带错误信息和原因的构造函数。 + * @param paramString 错误信息 + * @param paramThrowable 原因 + */ + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} diff --git a/NetworkFailureException.java b/NetworkFailureException.java new file mode 100644 index 0000000..d64d13b --- /dev/null +++ b/NetworkFailureException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; +/* + * 自定义异常类,表示网络操作失败。 + * 继承自Exception,用于在网络操作失败时抛出异常。 + */ +public class NetworkFailureException extends Exception { + private static final long serialVersionUID = 2107610287180234136L; + + public NetworkFailureException() { //默认构造函数。 + super(); + } +/* + * 带错误信息的构造函数。 + * @param paramString 错误信息 + */ + public NetworkFailureException(String paramString) { + super(paramString); + } +/* + * 带错误信息和原因的构造函数。 + * @param paramString 错误信息 + * @param paramThrowable 原因 + */ + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} -- 2.34.1 From 7ea5536fc63b7d5ae03f2dc89f7b14467dd91aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:20:44 +0800 Subject: [PATCH 06/44] Signed-off-by: --- GTaskASyncTask.java | 170 +++++++ GTaskClient.java | 807 +++++++++++++++++++++++++++++++ GTaskManager.java | 1065 +++++++++++++++++++++++++++++++++++++++++ GTaskSyncService.java | 198 ++++++++ 4 files changed, 2240 insertions(+) create mode 100644 GTaskASyncTask.java create mode 100644 GTaskClient.java create mode 100644 GTaskManager.java create mode 100644 GTaskSyncService.java diff --git a/GTaskASyncTask.java b/GTaskASyncTask.java new file mode 100644 index 0000000..ed52f7b --- /dev/null +++ b/GTaskASyncTask.java @@ -0,0 +1,170 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +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 { + + private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + public interface OnCompleteListener { //定义一个接口,用于在任务完成时通知监听器。 + void onComplete(); + } + + private Context mContext; + + private NotificationManager mNotifiManager; + + private GTaskManager mTaskManager; + + private OnCompleteListener mOnCompleteListener; +/* + * 构造函数,初始化上下文、通知管理器、任务管理器和完成监听器。 + * @param context 上下文 + * @param listener 完成监听器 + */ + public GTaskASyncTask(Context context, OnCompleteListener listener) { + // 初始化上下文 + mContext = context; + // 初始化完成监听器 + mOnCompleteListener = listener; + // 获取通知管理器 + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + // 获取任务管理器实例 + mTaskManager = GTaskManager.getInstance(); + } +/* + * 取消同步操作。 + */ + public void cancelSync() { + mTaskManager.cancelSync(); + } +/* + * 发布进度消息。 + */ + public void publishProgess(String message) { + publishProgress(new String[] { + message + }); + } +/* + * 显示通知。 + * @param tickerId 通知的提示文本ID + * @param content 通知的内容 + */ + private void showNotification(int tickerId, String content) { + // 创建一个新的Notification对象 + Notification notification = new Notification(R.drawable.notification, mContext + .getString(tickerId), System.currentTimeMillis()); + // 设置通知的默认灯光效果 + notification.defaults = Notification.DEFAULT_LIGHTS; + // 设置通知的标志,自动取消通知 + notification.flags = Notification.FLAG_AUTO_CANCEL; + // 创建PendingIntent,用于在用户点击通知时启动相应的Activity + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + // 如果通知不是成功通知,启动NotesPreferenceActivity + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + + } else { + // 如果通知是成功通知,启动NotesListActivity + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + // 设置通知的详细信息 + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, + pendingIntent); + // 发送通知 + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } +/* + * 在后台线程中执行同步操作。 + * @param unused 未使用的参数 + * @return 同步结果状态码 + */ + @Override + protected Integer doInBackground(Void... unused) { + // 发布登录进度消息 + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); + // 调用任务管理器的同步方法,并返回同步结果状态码 + return mTaskManager.sync(mContext, this); + } +/* + * 在UI线程中更新进度。 + * @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]); + } + } +/* + * 在UI线程中处理同步结果。 + * @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/GTaskClient.java b/GTaskClient.java new file mode 100644 index 0000000..d2d0669 --- /dev/null +++ b/GTaskClient.java @@ -0,0 +1,807 @@ +/* + * 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(); + + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; + + private DefaultHttpClient mHttpClient; + + private String mGetUrl; + + private String mPostUrl; + + private long mClientVersion; + + private boolean mLoggedin; + + private long mLastLoginTime; + + private int mActionId; + + private Account mAccount; + + private JSONArray mUpdateArray; +/* + * 私有构造函数,初始化成员变量。 + */ + private GTaskClient() { + mHttpClient = null; // 初始化HttpClient为null + mGetUrl = GTASK_GET_URL; // 初始化GET和POST请求的URL + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; // 初始化客户端版本号为-1 + mLoggedin = false; // 初始化登录状态为false + mLastLoginTime = 0; // 初始化最后一次登录时间为0 + mActionId = 1; // 初始化操作ID为1 + mAccount = null; // 初始化账户为null + mUpdateArray = null; // 初始化更新数组为null + } +/* + * 获取GTaskClient的单例实例。 + * @return GTaskClient的单例实例 + */ + public static synchronized GTaskClient getInstance() { + // 如果mInstance为null,创建一个新的GTaskClient实例 + if (mInstance == null) { + mInstance = new GTaskClient(); + } + // 返回GTaskClient的单例实例 + return mInstance; + } +/* + * 登录Google任务服务。 + * @param activity 当前活动 + * @return 是否登录成功 + */ + public boolean login(Activity activity) { + // we suppose that the cookie would expire after 5 minutes + // 假设cookie在5分钟后过期,需要重新登录 + // then we need to re-login + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // need to re-login after account switch + // 需要重新登录以切换账户 + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + // 如果已经登录,直接返回true + 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; + } + + // login with custom domain if necessary + // 如果账户不是gmail.com或googlemail.com,尝试使用自定义域名登录 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // try to login with google official url + // 尝试使用Google官方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; + // 获取AccountManager实例 + AccountManager accountManager = AccountManager.get(activity); + // 获取所有Google账户 + Account[] accounts = accountManager.getAccountsByType("com.google"); + // 如果没有可用的Google账户,记录错误日志并返回null + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + // 获取设置中的同步账户名称 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + // 遍历所有Google账户,查找与设置中同步账户名称匹配的账户 + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + // 如果找到匹配的账户,将其赋值给mAccount + if (account != null) { + mAccount = account; + } else { + // 如果没有找到匹配的账户,记录错误日志并返回null + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // get the token now + // 获取授权令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + // 获取授权令牌的Bundle + Bundle authTokenBundle = accountManagerFuture.getResult(); + // 从Bundle中获取授权令牌 + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 如果需要使授权令牌失效,使令牌失效并重新获取 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + // 如果获取授权令牌失败,记录错误日志并返回null + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; // 返回授权令牌 + } +/* + * 尝试登录Google任务服务。 + * @param activity 当前活动 + * @param authToken 授权令牌 + * @return 是否登录成功 + */ + private boolean tryToLoginGtask(Activity activity, String authToken) { + // 尝试使用当前授权令牌登录Google任务服务 + if (!loginGtask(authToken)) { + // maybe the auth token is out of date, now let's invalidate the + // token and try again + // 如果登录失败,可能是授权令牌已过期,使令牌失效并重新获取 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + // 使用新的授权令牌再次尝试登录Google任务服务 + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } +/* + * 使用授权令牌登录Google任务服务。 + * @param authToken 授权令牌 + * @return 是否登录成功 + */ + private boolean loginGtask(String authToken) { + // 设置连接超时和套接字超时 + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + // 创建DefaultHttpClient实例 + mHttpClient = new DefaultHttpClient(httpParameters); + // 创建BasicCookieStore实例并设置到HttpClient中 + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + // 设置HttpClient不使用Expect-Continue + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // login gtask + // 登录Google任务服务 + try { + // 构建登录URL + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the cookie now + // 获取cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // get the client version + // 获取客户端版本 + 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) { + // simply catch all exceptions + // 捕获所有异常 + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } +/* + * 获取操作ID并递增。 + * @return 当前操作ID + */ + private int getActionId() { + // 返回当前操作ID并递增 + return mActionId++; + } +/* + * 创建HttpPost请求。 + * @return 创建的HttpPost请求 + */ + private HttpPost createHttpPost() { + // 创建HttpPost请求,设置请求URL + HttpPost httpPost = new HttpPost(mPostUrl); + // 设置请求头,指定Content-Type为application/x-www-form-urlencoded,字符集为UTF-8 + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + // 设置请求头,指定AT为1 + return httpPost; + // 返回创建的HttpPost请求 + } +/* + * 从HttpEntity中获取响应内容并返回字符串形式。 + * 支持处理gzip和deflate压缩编码。 + * @param entity HTTP响应实体 + * @return 响应内容的字符串形式 + * @throws IOException 如果读取输入流时发生错误 + */ + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + // 检查HttpEntity是否包含Content-Encoding头部 + if (entity.getContentEncoding() != null) { + // 获取Content-Encoding的值 + contentEncoding = entity.getContentEncoding().getValue(); + // 记录日志,显示当前的Content-Encoding + Log.d(TAG, "encoding: " + contentEncoding); + } + // 获取HttpEntity的原始输入流 + InputStream input = entity.getContent(); + // 如果Content-Encoding是gzip,则使用GZIPInputStream解压缩 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + // 如果Content-Encoding是deflate,则使用InflaterInputStream解压缩 + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + // 创建InputStreamReader,用于将字节流转换为字符流 + InputStreamReader isr = new InputStreamReader(input); + // 创建BufferedReader,用于逐行读取字符流 + BufferedReader br = new BufferedReader(isr); + // 创建StringBuilder,用于存储读取到的内容 + StringBuilder sb = new StringBuilder(); + // 循环读取输入流中的每一行 + while (true) { + String buff = br.readLine(); + // 如果读取到null,表示已经到达流的末尾,返回StringBuilder中的内容 + if (buff == null) { + return sb.toString(); + } + // 将读取到的行追加到StringBuilder中 + sb = sb.append(buff); + } + } finally { + // 确保输入流在方法结束时被关闭 + input.close(); + } + } +/* + * 发送POST请求并返回响应的JSONObject。 + * @param js 要发送的请求数据,以JSONObject形式表示 + * @return 服务器响应的JSONObject + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如未登录或响应内容无法转换为JSONObject) + */ + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + // 检查用户是否已登录,如果未登录则抛出异常 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + // 创建HttpPost对象 + HttpPost httpPost = createHttpPost(); + try { + // 创建一个LinkedList来存储请求参数 + LinkedList list = new LinkedList(); + // 将请求数据(JSONObject)转换为字符串,并添加到请求参数列表中 + list.add(new BasicNameValuePair("r", js.toString())); + // 创建UrlEncodedFormEntity对象,用于将请求参数编码为表单格式 + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + // 将请求实体设置到HttpPost对象中 + httpPost.setEntity(entity); + // execute the post + // 执行POST请求 + HttpResponse response = mHttpClient.execute(httpPost); + // 获取响应内容并转换为字符串 + String jsString = getResponseContent(response.getEntity()); + // 将响应内容字符串转换为JSONObject并返回 + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + // 捕获并记录HTTP协议异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + // 捕获并记录IO异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + 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 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void createTask(Task task) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储任务的创建操作 + JSONArray actionList = new JSONArray(); + + // action_list + // 将任务的创建操作添加到actionList中 + actionList.put(task.getCreateAction(getActionId())); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + // 发送POST请求,并将响应内容转换为JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取结果数组,并获取第一个结果 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 从结果中获取新创建任务的ID,并设置到任务对象中 + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) {、 + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handing jsonobject failed"); + } + } +/* + * 创建一个新的任务列表,并将其上传到服务器。 + * @param tasklist 要创建的任务列表对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void createTaskList(TaskList tasklist) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储任务列表的创建操作 + JSONArray actionList = new JSONArray(); + + // action_list + // 将任务列表的创建操作添加到actionList中 + actionList.put(tasklist.getCreateAction(getActionId())); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + // 发送POST请求,并将响应内容转换为JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取结果数组,并获取第一个结果 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 从结果中获取新创建任务列表的ID,并设置到任务列表对象中 + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handing jsonobject failed"); + } + } +/* + * 提交所有未完成的更新操作到服务器。 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void commitUpdate() throws NetworkFailureException { + // 检查是否有未完成的更新操作 + if (mUpdateArray != null) { + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + + // action_list + // 将未完成的更新操作数组添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求 + postRequest(jsPost); + // 提交成功后,清空更新操作数组 + mUpdateArray = null; + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("commit update: handing jsonobject failed"); + } + } + } +/* + * 将一个节点添加到更新操作数组中,并在必要时提交更新操作。 + * @param node 要添加的节点对象 + * @throws NetworkFailureException 如果网络请求失败 + */ + public void addUpdateNode(Node node) throws NetworkFailureException { + // 检查节点是否为空 + if (node != null) { + // too many update items may result in an error + // set max to 10 items + // 如果更新操作数组中已经有超过10个项目,则提交更新操作 + // 避免过多的更新操作导致错误 + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate(); + } + // 如果更新操作数组为空,则创建一个新的JSONArray + if (mUpdateArray == null) + mUpdateArray = new JSONArray(); + // 将节点的更新操作添加到更新操作数组中 + mUpdateArray.put(node.getUpdateAction(getActionId())); + } + } +/* + * 将任务从一个任务列表移动到另一个任务列表。 + * @param task 要移动的任务对象 + * @param preParent 任务的原始父任务列表 + * @param curParent 任务的新父任务列表 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void moveTask(Task task, TaskList preParent, TaskList curParent) + throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储任务的移动操作 + JSONArray actionList = new JSONArray(); + // 创建一个JSONObject,用于存储具体的移动操作 + JSONObject action = new JSONObject(); + + // action_list + // 设置移动操作的类型为"move" + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + // 设置移动操作的ID + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 设置要移动的任务的ID + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); + // 如果任务在同一个任务列表中移动,并且不是第一个任务,则设置前一个兄弟任务的ID + if (preParent == curParent && task.getPriorSibling() != null) { + // put prioring_sibing_id only if moving within the tasklist and + // it is not the first one + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } + // 设置任务的原始父任务列表的ID + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + // 设置任务的新父任务列表的ID + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); + // 如果任务在不同的任务列表之间移动,则设置目标任务列表的ID + if (preParent != curParent) { + // put the dest_list only if moving between tasklists + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + // 将移动操作添加到actionList中 + actionList.put(action); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求 + postRequest(jsPost); + + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("move task: handing jsonobject failed"); + } + } +/* + * 删除一个节点,并将其标记为已删除状态。 + * @param node 要删除的节点对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void deleteNode(Node node) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储节点的删除操作 + JSONArray actionList = new JSONArray(); + + // action_list + // 将节点标记为已删除 + node.setDeleted(true); + // 获取节点的更新操作,并将其添加到actionList中 + actionList.put(node.getUpdateAction(getActionId())); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求 + postRequest(jsPost); + // 提交成功后,清空更新操作数组 + mUpdateArray = null; + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("delete node: handing jsonobject failed"); + } + } +/* + * 获取所有任务列表,并返回一个包含任务列表的JSONArray。 + * @return 包含任务列表的JSONArray + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如未登录或JSON处理失败) + */ + public JSONArray getTaskLists() throws NetworkFailureException { + // 检查用户是否已登录,如果未登录则抛出异常 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + try { + // 创建一个HttpGet对象,用于发送GET请求 + HttpGet httpGet = new HttpGet(mGetUrl); + HttpResponse response = null; + // 执行GET请求,并获取响应 + response = mHttpClient.execute(httpGet); + + // get the task list + // 获取响应内容并转换为字符串 + String resString = getResponseContent(response.getEntity()); + // 从响应内容中提取包含任务列表的JSON字符串 + 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 + JSONObject js = new JSONObject(jsString); + // 从JSONObject中获取任务列表的JSONArray并返回 + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); + } catch (ClientProtocolException e) { + // 捕获并记录HTTP协议异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (IOException e) { + // 捕获并记录IO异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task lists: handing jasonobject failed"); + } + } +/* + * 获取指定任务列表中的所有任务,并返回一个包含任务的JSONArray。 + * @param listGid 任务列表的ID + * @return 包含任务的JSONArray + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储获取任务列表的操作 + JSONArray actionList = new JSONArray(); + // 创建一个JSONObject,用于存储具体的获取操作 + JSONObject action = new JSONObject(); + + // action_list + // 设置获取操作的类型为"getall" + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + // 设置获取操作的ID + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 设置要获取的任务列表的ID + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + // 设置是否获取已删除的任务,这里设置为false + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + // 将获取操作添加到actionList中 + actionList.put(action); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求,并将响应内容转换为JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取任务列表的JSONArray并返回 + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task list: handing jsonobject failed"); + } + } +/* + * 获取当前同步的账户。 + * @return 当前同步的账户对象 + */ + public Account getSyncAccount() { + return mAccount; + } +/* + * 重置更新操作数组。 + */ + public void resetUpdateArray() { + mUpdateArray = null; + } +} diff --git a/GTaskManager.java b/GTaskManager.java new file mode 100644 index 0000000..cf77515 --- /dev/null +++ b/GTaskManager.java @@ -0,0 +1,1065 @@ +/* + * 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. + */ +/* + * GTaskManager 负责管理本地笔记数据库与 Google Tasks 之间的同步过程。 + */ +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; + +/* + * GTaskManager 负责管理本地笔记数据库与 Google Tasks 之间的同步过程。 + */ +public class GTaskManager { + private static final String TAG = GTaskManager.class.getSimpleName(); + // 同步状态常量 + public static final int STATE_SUCCESS = 0; + + public static final int STATE_NETWORK_ERROR = 1; + + public static final int STATE_INTERNAL_ERROR = 2; + + public static final int STATE_SYNC_IN_PROGRESS = 3; + + public static final int STATE_SYNC_CANCELLED = 4; + // 单例实例 + private static GTaskManager mInstance = null; + // 用于获取认证令牌的活动上下文 + private Activity mActivity; + // 应用程序上下文 + private Context mContext; + // 内容解析器用于数据库操作 + private ContentResolver mContentResolver; + // 同步状态标志 + private boolean mSyncing; + + private boolean mCancelled; + // 用于管理任务列表、节点、元数据和 GID 与 NID 映射的哈希表 + private HashMap mGTaskListHashMap; + + private HashMap mGTaskHashMap; + + private HashMap mMetaHashMap; + + private TaskList mMetaList; + + private HashSet mLocalDeleteIdMap; + + private HashMap mGidToNid; + + private HashMap mNidToGid; + + private GTaskManager() { + // 初始化同步状态标志,表示当前没有正在进行同步 + mSyncing = false; + // 初始化取消同步标志,表示当前没有取消同步 + mCancelled = false; + // 初始化用于存储 Google Task 列表的哈希表 + mGTaskListHashMap = new HashMap();、 + // 初始化用于存储 Google Task 节点的哈希表 + mGTaskHashMap = new HashMap(); + // 初始化用于存储元数据的哈希表 + mMetaHashMap = new HashMap(); + // 初始化元数据列表,初始值为 null + mMetaList = null; + // 初始化用于存储本地删除的笔记 ID 的集合 + mLocalDeleteIdMap = new HashSet(); + // 初始化用于存储 Google Task ID 到本地笔记 ID 映射的哈希表 + mGidToNid = new HashMap(); + // 初始化用于存储本地笔记 ID 到 Google Task ID 映射的哈希表 + mNidToGid = new HashMap(); + } +/* + * 获取 GTaskManager 的单例实例。 + * 该方法使用 synchronized 关键字确保线程安全,防止多个线程同时创建实例。 + * @return GTaskManager 的单例实例 + */ + public static synchronized GTaskManager getInstance() { + // 检查单例实例是否已经创建 + if (mInstance == null) { + // 如果未创建,则创建一个新的 GTaskManager 实例 + mInstance = new GTaskManager(); + } + return mInstance; // 返回单例实例 + } +/* + * 设置活动上下文,用于获取认证令牌。 + * 该方法使用 synchronized 关键字确保线程安全,防止多个线程同时修改 mActivity 变量。 + * @param activity 活动上下文 + */ + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + // 将传入的活动上下文赋值给 mActivity 变量 + mActivity = activity; + } +/* + * 同步本地笔记数据库与 Google Tasks。 + * 该方法负责初始化同步过程,处理同步过程中的各种异常,并返回同步结果状态。 + * @param context 应用程序上下文 + * @param asyncTask 异步任务对象,用于发布同步进度 + * @return 同步结果状态,可能的值为 STATE_SUCCESS, STATE_NETWORK_ERROR, STATE_INTERNAL_ERROR, STATE_SYNC_IN_PROGRESS, STATE_SYNC_CANCELLED + */ + 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 实例并重置更新数组 + GTaskClient client = GTaskClient.getInstance(); + client.resetUpdateArray(); + + // login google task + // 登录 Google Task + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // get the task list from google + // 从 Google 获取任务列表 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // do content sync work + // 执行内容同步工作 + 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; // 返回同步结果状态 + } +/* + * 初始化 Google Task 列表。 + * 该方法从 Google Task 获取任务列表,并初始化元数据列表和任务列表。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void initGTaskList() throws NetworkFailureException { + // 检查是否已经取消同步 + if (mCancelled) + return; + // 获取 GTaskClient 实例 + GTaskClient client = GTaskClient.getInstance(); + try { + // 从 Google Task 获取任务列表 + JSONArray jsTaskLists = client.getTaskLists(); + + // init meta list first + // 首先初始化元数据列表 + mMetaList = null; //初始化元数据列表,将其设置为 null。 + for (int i = 0; i < jsTaskLists.length(); i++) { + //遍历任务列表,逐个处理每个任务列表。 + JSONObject object = jsTaskLists.getJSONObject(i); + //获取当前任务列表的 JSONObject + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + //获取当前任务列表的 Google Task ID + //获取当前任务列表的名称。 + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + // 检查是否是元数据列表 + //如果是,则初始化元数据列表并加载元数据。 + if (name + .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + //创建一个新的 TaskList 对象作为元数据列表。 + mMetaList.setContentByRemoteJSON(object); + //使用远程 JSON 数据设置元数据列表的内容。 + + // load meta data + // 加载元数据 + JSONArray jsMetas = client.getTaskList(gid); + //获取元数据列表中的元数据,返回一个 JSONArray。 + for (int j = 0; j < jsMetas.length(); j++) { + //遍历元数据列表,逐个处理每个元数据。 + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + //创建一个新的 MetaData 对象 + metaData.setContentByRemoteJSON(object); + //使用远程 JSON 数据设置元数据的内容 + if (metaData.isWorthSaving()) { + //检查元数据是否值得保存。如果值得保存,则将其添加到元数据列表中,并更新 mMetaHashMap + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + //检查元数据的 Google Task ID 是否存在。如果存在,则将其添加到 mMetaHashMap 中 + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // create meta list if not existed + // 如果元数据列表不存在,则创建一个新的元数据列表 + if (mMetaList == null) { + //检查元数据列表是否为空。如果为空,则创建一个新的元数据列表并将其添加到 Google Task 中。 + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META); + //设置元数据列表的名称 + GTaskClient.getInstance().createTaskList(mMetaList); + //将元数据列表添加到 Google Task 中 + } + + // init task list + // 初始化任务列表 + for (int i = 0; i < jsTaskLists.length(); i++) { + //再次遍历任务列表,逐个处理每个任务列表 + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + // 检查是否是 MIUI 文件夹前缀的任务列表 + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) + && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META)) { + //检查当前任务列表是否是 MIUI 文件夹前缀的任务列表,并且不是元数据列表。如果是,则初始化任务列表并加载任务 + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // load tasks + // 加载任务 + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + //遍历任务列表,逐个处理每个任务。 + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + //检查任务是否值得保存。如果值得保存,则将其添加到任务列表中,并更新 mGTaskHashMap。 + 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: handing JSONObject failed"); + } + } +/* + * 同步内容。 + * 该方法负责同步本地数据库中的笔记与 Google Tasks 中的任务。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + // 清空本地删除的笔记 ID 集合 + mLocalDeleteIdMap.clear(); + // 检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + if (mCancelled) { + return; + } + + // for local deleted note + // 处理本地删除的笔记 + //使用 try-finally 结构确保在查询结束后关闭游标。 + 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); + //查询本地删除的笔记。查询条件为 type 不等于 Notes.TYPE_SYSTEM 且 parent_id 等于 Notes.ID_TRASH_FOLER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个笔记。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //获取当前笔记的 Google Task ID。 + node = mGTaskHashMap.get(gid); + //从 mGTaskHashMap 中获取对应的节点。 + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并执行远程删除操作。 + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); + //执行远程删除操作。 + } + + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + //将当前笔记的本地 ID 添加到 mLocalDeleteIdMap 中。 + } + } else { + Log.w(TAG, "failed to query trash folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // sync folder first + syncFolder(); + + // for note existing in database + // 处理数据库中存在的笔记 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + //查询数据库中存在的笔记。查询条件为 type 等于 Notes.TYPE_NOTE 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个笔记。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //获取当前笔记的 Google Task ID。 + node = mGTaskHashMap.get(gid); + //从 mGTaskHashMap 中获取对应的节点。 + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射,然后获取同步操作类型。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + //更新 Google Task ID 到本地笔记 ID 的映射。 + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + //更新本地笔记 ID 到 Google Task ID 的映射。 + syncType = node.getSyncAction(c); + //获取当前笔记的同步操作类型。 + } else { + //如果节点不存在,则根据 Google Task ID 是否为空来确定同步操作类型。 + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + // 本地添加 + //检查 Google Task ID 是否为空。如果为空,则表示本地添加操作。 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + //设置同步操作类型为远程添加。 + } else { + // remote delete + // 远程删除 + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c);//执行同步操作。 + } + } else { + Log.w(TAG, "failed to query existing note in database"); + } + + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // go through remaining items + // 处理剩余的项目 + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + //获取 mGTaskHashMap 的迭代器。 + while (iter.hasNext()) { + //遍历 mGTaskHashMap,逐个处理剩余的项目。 + Map.Entry entry = iter.next(); + //获取当前的键值对。 + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + //执行本地添加操作 + } + + // mCancelled can be set by another thread, so we neet to check one by + // one + // clear local delete table + // 检查是否已经取消同步 + if (!mCancelled) { + // 批量删除本地删除的笔记 + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + //批量删除本地删除的笔记。如果删除失败,则抛出 ActionFailureException。 + } + + // refresh local sync id + // 刷新本地同步 ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } +/* + * 同步文件夹。 + * 该方法负责同步本地数据库中的文件夹与 Google Tasks 中的任务列表。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + if (mCancelled) { + return; + } + + // for root folder + //处理根文件夹。 + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + //查询根文件夹。查询条件为 _id 等于 Notes.ID_ROOT_FOLDER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则处理查询结果。 + c.moveToNext(); + //移动游标到下一行。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //获取当前文件夹的 Google Task ID。 + node = mGTaskHashMap.get(gid); + //从 mGTaskHashMap 中获取对应的节点。 + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + //更新 Google Task ID 到本地笔记 ID 的映射。 + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // for system folder, only update remote name if necessary + //更新本地笔记 ID 到 Google Task ID 的映射。 + 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; + } + } + + // for call-note folder + //处理通话记录文件夹。 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + //查询通话记录文件夹。查询条件为 _id 等于 Notes.ID_CALL_RECORD_FOLDER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则处理查询结果。 + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); + // for system folder, only update remote name if + // necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE)) + 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; + } + } + + // for local existing folders + //处理本地存在的文件夹。 + 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"); + //查询本地存在的文件夹。查询条件为 type 等于 Notes.TYPE_FOLDER 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个文件夹。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射,然后获取同步操作类型。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + //如果节点不存在,则根据 Google Task ID 是否为空来确定同步操作类型。 + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + //检查 Google Task ID 是否为空。如果为空,则表示本地添加操作 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + //如果 Google Task ID 不为空,则表示远程删除操作。 + // remote delete + 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; + } + } + + // for remote add folders + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + //获取 mGTaskListHashMap 的迭代器。 + while (iter.hasNext()) { + //遍历 mGTaskListHashMap,逐个处理远程添加的文件夹。 + Map.Entry entry = iter.next(); + //获取当前的键值对 + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + //检查 mGTaskHashMap 中是否包含该节点。如果包含,则从 mGTaskHashMap 中移除该节点,并执行本地添加操作。 + 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: + // 本地删除操作。从 mMetaHashMap 中获取元数据。如果元数据存在,则调用 GTaskClient.getInstance().deleteNode(meta) 方法删除元数据。将本地笔记 ID 添加到 mLocalDeleteIdMap 中。 + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + break; + case Node.SYNC_ACTION_DEL_REMOTE: + // 远程删除操作。从 mMetaHashMap 中获取元数据。如果元数据存在,则调用 GTaskClient.getInstance().deleteNode(meta) 方法删除元数据。调用 GTaskClient.getInstance().deleteNode(node) 方法删除节点。 + 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: + // merging both modifications maybe a good idea + // right now just use local update simply + // 更新冲突操作 + // 合并两个修改可能是一个好主意 + // 现在只是简单地使用本地更新 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: + // 无操作 + break; + case Node.SYNC_ACTION_ERROR: + default: + // 未知同步操作类型 + throw new ActionFailureException("unkown sync action type"); + } + } +/* + * 添加本地节点。 + * 该方法负责将远程节点添加到本地数据库中。 + * @param node 远程节点对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote;//声明一个 SqlNote 变量,用于存储本地笔记。 + if (node instanceof TaskList) { + //检查节点是否是任务列表。如果是任务列表,则根据节点的名称创建相应的本地笔记。 + if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { + //检查节点名称是否是根文件夹。如果是根文件夹,则创建根文件夹的本地笔记。 + sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); + } else if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { + //检查节点名称是否是通话记录文件夹。如果是通话记录文件夹,则创建通话记录文件夹的本地笔记。 + sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); + } else { + //其他文件夹。创建新的本地笔记,并设置内容和父节点 ID。 + sqlNote = new SqlNote(mContext); + sqlNote.setContent(node.getLocalJSONFromContent()); + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + } + } else { + //如果是任务。创建新的本地笔记,并设置内容和父节点 ID。 + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + //处理 JSON 解析异常。 + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + //检查是否包含笔记信息。如果包含笔记信息,则检查笔记 ID 是否可用。 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + //检查笔记信息是否包含 ID。如果包含 ID,则检查 ID 是否可用。 + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // the id is not available, have to create a new one + //检查笔记 ID 是否存在于本地数据库中。如果存在,则移除笔记 ID。 + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + //检查是否包含数据信息。如果包含数据信息,则检查数据 ID 是否可用。 + 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)) { + //检查数据信息是否包含 ID。如果包含 ID,则检查 ID 是否可用。 + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // the data id is not available, have to create + // a new one + //检查数据 ID 是否存在于本地数据库中。如果存在,则移除数据 ID。 + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js);//设置本地笔记的内容。 + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + //获取父节点的本地 ID。 + if (parentId == null) { + //检查父节点的本地 ID 是否存在。如果不存在,则抛出 ActionFailureException 异常。 + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // create the local node + sqlNote.setGtaskId(node.getGid());//更新 Google Task ID 到本地笔记 ID 的映射。 + sqlNote.commit(false); + + // update gid-nid mapping + mGidToNid.put(node.getGid(), sqlNote.getId());//更新本地笔记 ID 到 Google Task ID 的映射。 + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote);//更新元数据。 + } +/* + * 更新本地节点。 + * 该方法负责将远程节点的更新同步到本地数据库中。 + * @param node 远程节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote; + // update the note locally + //声明一个 SqlNote 变量,用于存储本地笔记。 + sqlNote = new SqlNote(mContext, c); + //创建一个新的 SqlNote 对象,并使用游标 c 初始化。 + sqlNote.setContent(node.getLocalJSONFromContent()); + //设置本地笔记的内容为远程节点的本地 JSON 内容。 + Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + //获取父节点的本地 ID。如果节点是任务,则从 mGidToNid 映射中获取父节点的本地 ID。如果节点不是任务,则将父节点 ID 设置为根文件夹的 ID。 + if (parentId == null) { + //检查父节点的本地 ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue());//设置本地笔记的父节点 ID。 + sqlNote.commit(true);//提交本地笔记的更新。 + + // update meta info + //更新元数据信息。 + updateRemoteMeta(node.getGid(), sqlNote); + } +/* + * 添加远程节点。 + * 该方法负责将本地笔记添加到 Google Tasks 中。 + * @param node 远程节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + //创建一个新的 SqlNote 对象,并使用游标 c 初始化。 + Node n; + //声明一个 Node 变量,用于存储远程节点 + + // update remotely + if (sqlNote.isNoteType()) { + //检查本地笔记是否是笔记类型。如果是笔记类型,则创建一个新的 Task 对象,并设置内容。 + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + String parentGid = mNidToGid.get(sqlNote.getParentId()); + if (parentGid == null) { + //检查父任务列表的 Google Task ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 + 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; + //将 Task 对象赋值给 Node 变量。 + + // add meta + //添加元数据。 + updateRemoteMeta(task.getGid(), sqlNote); + } else { + //如果是文件夹类型。创建一个新的 TaskList 对象,并设置内容。 + TaskList tasklist = null; + + // we need to skip folder if it has already existed + String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;//初始化文件夹名称 + if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) + //检查本地笔记的 ID 是否是根文件夹的 ID。如果是根文件夹,则设置文件夹名称为默认文件夹名称。 + folderName += GTaskStringUtils.FOLDER_DEFAULT; + else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) + //检查本地笔记的 ID 是否是通话记录文件夹的 ID。如果是通话记录文件夹,则设置文件夹名称为通话记录文件夹名称。 + folderName += GTaskStringUtils.FOLDER_CALL_NOTE; + else + //其他文件夹。设置文件夹名称为本地笔记的片段。 + folderName += sqlNote.getSnippet(); + + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + //遍历 mGTaskListHashMap,查找匹配的文件夹。 + Map.Entry entry = iter.next(); + String gid = entry.getKey(); + TaskList list = entry.getValue(); + + if (list.getName().equals(folderName)) { + //检查任务列表的名称是否与文件夹名称匹配。如果匹配,则将任务列表赋值给 tasklist 变量,并从 mGTaskHashMap 中移除该节点。 + tasklist = list; + if (mGTaskHashMap.containsKey(gid)) { + //检查 mGTaskHashMap 中是否包含该节点。如果包含,则从 mGTaskHashMap 中移除该节点。 + mGTaskHashMap.remove(gid); + } + break; + } + } + + // no match we can add now + if (tasklist == null) { + //如果没有匹配的文件夹,则可以添加。创建一个新的 TaskList 对象,并设置内容。 + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist;//将 TaskList 对象赋值给 Node 变量。 + } + + // update local note + sqlNote.setGtaskId(n.getGid());//设置本地笔记的 Google Task ID + sqlNote.commit(false);//提交本地笔记的更新。 + sqlNote.resetLocalModified();//重置本地笔记的修改标志。 + sqlNote.commit(true);//重置本地笔记的修改标志。 + + // gid-id mapping + mGidToNid.put(n.getGid(), sqlNote.getId());//更新 Google Task ID 到本地笔记 ID 的映射。 + mNidToGid.put(sqlNote.getId(), n.getGid());//更新本地笔记 ID 到 Google Task ID 的映射。 + } +/* + * 更新远程节点。 + * 该方法负责将本地笔记的更新同步到 Google Tasks 中。 + * @param node 远程节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c);//创建一个新的 SqlNote 对象,并使用游标 c 初始化。 + + // update remotely + //使用本地笔记的内容更新远程节点的内容。 + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + //将更新后的节点添加到 GTaskClient 的更新列表中。 + + // update meta + //更新元数据。 + updateRemoteMeta(node.getGid(), sqlNote); + + // move task if necessary + if (sqlNote.isNoteType()) { + //检查本地笔记是否是笔记类型。如果是笔记类型,则处理任务的移动 + Task task = (Task) node; + TaskList preParentList = task.getParent(); + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); + if (curParentGid == null) { + //检查父任务列表的 Google Task ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 + 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); + } + } + + // clear local modified flag + sqlNote.resetLocalModified();//清除本地笔记的修改标志。 + sqlNote.commit(true);//提交本地笔记的更新。 + } +/* + * 更新远程元数据。 + * 该方法负责将本地笔记的元数据同步到 Google Tasks 中。 + * @param gid Google Task ID + * @param 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。 + * 该方法负责刷新本地笔记的同步 ID,确保本地笔记的同步 ID 与 Google Tasks 中的最新数据一致。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void refreshLocalSyncId() throws NetworkFailureException { + // 检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + if (mCancelled) { + return; + } + + // get the latest gtask list + mGTaskHashMap.clear();//清空 mGTaskHashMap + mGTaskListHashMap.clear();//清空 mGTaskListHashMap。 + mMetaHashMap.clear();//清空 mMetaHashMap。 + initGTaskList();//初始化 Google Task 列表。 + + Cursor c = null;//声明一个游标变量 c,用于存储查询结果。 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id<>?)", new String[] { + //查询本地笔记。查询条件为 type 不等于 Notes.TYPE_SYSTEM 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 + 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 中移除该节点,并更新本地笔记的同步 ID。 + 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 { + //如果节点不存在,则记录错误日志并抛出 ActionFailureException 异常。 + Log.e(TAG, "something is missed"); + throw new ActionFailureException( + "some local items don't have gid after sync"); + } + } + } else { + Log.w(TAG, "failed to query local note to refresh sync id"); + } + } finally { + if (c != null) { + //检查游标是否为空。如果不为空,则关闭游标。 + c.close(); + c = null; + } + } + } +/* + * 获取同步账户名称。 + * 该方法返回当前用于同步的 Google 账户名称。 + * @return 同步账户名称 + */ + public String getSyncAccount() { + // 获取 GTaskClient 的单例实例,并返回同步账户的名称 + return GTaskClient.getInstance().getSyncAccount().name; + } +/* + * 取消同步。 + * 该方法用于设置取消同步标志,以便在同步过程中停止同步操作。 + */ + public void cancelSync() { + // 设置取消同步标志为 true + mCancelled = true; + } +} diff --git a/GTaskSyncService.java b/GTaskSyncService.java new file mode 100644 index 0000000..7ba4834 --- /dev/null +++ b/GTaskSyncService.java @@ -0,0 +1,198 @@ +/* + * 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.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +/* + * GTaskSyncService类是一个服务类,用于处理Google任务的同步操作。 + * 该类继承自Service,并在后台执行同步任务。 + */ +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) { + // 创建一个新的GTaskASyncTask对象 + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + public void onComplete() { + // 同步任务完成后,将mSyncTask置为null + mSyncTask = null; + // 发送广播通知同步完成 + sendBroadcast(""); + // 停止服务 + stopSelf(); + } + }); + // 发送广播通知同步开始 + sendBroadcast(""); + // 执行同步任务 + mSyncTask.execute(); + } + } +/* + * 取消同步操作。 + */ + private void cancelSync() { + // 如果同步任务正在执行 + if (mSyncTask != null) { + // 调用cancelSync方法取消同步 + mSyncTask.cancelSync(); + } + } +/* + * 服务创建时调用,初始化同步任务。 + */ + @Override + public void onCreate() { + // 将同步任务置为null + mSyncTask = null; + } +/* + * 服务启动时调用,处理启动和取消同步的命令。 + * @param intent 启动服务的意图 + * @param flags 启动标志 + * @param startId 启动ID + * @return 服务启动模式 + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // 获取意图中的额外数据 + Bundle bundle = intent.getExtras(); + // 如果额外数据不为空且包含ACTION_STRING_NAME + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据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; + } + // 返回START_STICKY,表示服务在异常终止后会自动重启 + return START_STICKY; + } + // 如果额外数据为空或不包含ACTION_STRING_NAME,调用父类的onStartCommand方法 + return super.onStartCommand(intent, flags, startId); + } +/* + * 内存不足时调用,取消同步任务。 + */ + @Override + public void onLowMemory() { + // 如果同步任务正在执行 + if (mSyncTask != null) { + // 调用cancelSync方法取消同步 + mSyncTask.cancelSync(); + } + } +/* + * 绑定服务时调用,返回null。 + * @param intent 绑定服务的意图 + * @return null + */ + public IBinder onBind(Intent intent) { + // 返回null,表示不支持绑定服务 + return null; + } +/* + * 发送广播,通知同步状态和进度。 + * @param msg 进度消息 + */ + public void sendBroadcast(String msg) { + // 更新同步进度字符串 + mSyncProgress = msg; + // 创建一个新的Intent对象,用于发送广播 + 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); + } +/* + * 启动同步操作的静态方法。 + * @param activity 启动同步的Activity + */ + public static void startSync(Activity activity) { + // 设置活动上下文 + GTaskManager.getInstance().setActivityContext(activity); + // 创建一个新的Intent对象,用于启动服务 + Intent intent = new Intent(activity, GTaskSyncService.class); + // 添加启动同步的命令 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + // 启动服务 + activity.startService(intent); + } +/* + * 取消同步操作的静态方法。 + * @param context 取消同步的上下文 + */ + public static void cancelSync(Context context) { + // 创建一个新的Intent对象,用于启动服务 + Intent intent = new Intent(context, GTaskSyncService.class); + // 添加取消同步的命令 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + // 启动服务 + context.startService(intent); + } +/* + * 检查是否正在同步。 + * @return 如果正在同步,返回true;否则返回false + */ + public static boolean isSyncing() { + // 如果mSyncTask不为null,表示正在同步 + return mSyncTask != null; + } +/* + * 获取同步进度字符串。 + * @return 同步进度字符串 + */ + public static String getProgressString() { + // 返回当前的同步进度字符串 + return mSyncProgress; + } +} -- 2.34.1 From ca93e9dec327cce768572efe1987d7f66de2f991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:21:33 +0800 Subject: [PATCH 07/44] Signed-off-by: --- Note.java | 382 ++++++++++++++++++++++++++++++++++ WorkingNote.java | 532 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 914 insertions(+) create mode 100644 Note.java create mode 100644 WorkingNote.java diff --git a/Note.java b/Note.java new file mode 100644 index 0000000..27b9dab --- /dev/null +++ b/Note.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.net.Uri; +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.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; + +import java.util.ArrayList; + +/* + * Note 类用于管理笔记的创建、更新和同步。 + * 该类提供了创建新笔记、设置笔记内容、同步笔记等功能。 + */ +public class Note { + private ContentValues mNoteDiffValues; + private NoteData mNoteData; + private static final String TAG = "Note"; + /** + * Create a new note id for adding a new note to databases + */ + /* + * 创建一个新的笔记 ID,用于将新笔记添加到数据库中。 + * + * @param context 应用程序上下文 + * @param folderId 文件夹 ID + * @return 新创建的笔记 ID + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // Create a new note in the database + // 创建一个新的笔记到数据库中 + ContentValues values = new ContentValues(); + //创建一个新的 ContentValues 对象,用于存储笔记的初始值。 + long createdTime = System.currentTimeMillis(); + //获取当前时间戳,作为笔记的创建时间和修改时间。 + values.put(NoteColumns.CREATED_DATE, createdTime); + //将创建时间存储到 ContentValues 对象中。 + values.put(NoteColumns.MODIFIED_DATE, createdTime); + //将修改时间存储到 ContentValues 对象中。 + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + //将笔记类型存储到 ContentValues 对象中。 + values.put(NoteColumns.LOCAL_MODIFIED, 1); + //将本地修改标志设置为 1,表示笔记已被修改。 + values.put(NoteColumns.PARENT_ID, folderId); + //将文件夹 ID 存储到 ContentValues 对象中。 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + //将 ContentValues 对象插入到数据库中,并返回新创建笔记的 URI。 + long noteId = 0;//初始化笔记 ID 变量。 + try { + //尝试从 URI 中获取笔记 ID。如果发生 NumberFormatException 异常,则记录错误日志,并将笔记 ID 设置为 0。 + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + //检查笔记 ID 是否为 -1。如果为 -1,则抛出 IllegalStateException 异常。 + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId;//返回新创建的笔记 ID。 + } +/* + * 构造函数,初始化笔记的差异值和笔记数据。 + */ + public Note() { + //// 初始化笔记的差异值 + mNoteDiffValues = new ContentValues(); + // // 初始化笔记数据 + mNoteData = new NoteData(); + } +/* + * 设置笔记的差异值。 + * @param key 键 + * @param value 值 + */ + public void setNoteValue(String key, String value) { + // 将键值对存储到笔记的差异值中 + mNoteDiffValues.put(key, value); + // 设置本地修改标志为 1,表示笔记已被修改 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 设置修改时间为当前时间 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } +/* + * 设置笔记的文本数据。 + * @param key 键 + * @param value 值 + */ + public void setTextData(String key, String value) { + // 调用 NoteData 类的 setTextData 方法,设置文本数据 + mNoteData.setTextData(key, value); + } +/* + * 设置笔记的文本数据 ID。 + * @param id 文本数据 ID + */ + public void setTextDataId(long id) { + // 调用 NoteData 类的 setTextDataId 方法,设置文本数据 ID + mNoteData.setTextDataId(id); + } +/* + * 获取笔记的文本数据 ID。 + * @return 文本数据 ID + */ + public long getTextDataId() { + // 返回 NoteData 类的 mTextDataId 字段 + return mNoteData.mTextDataId; + } +/* + * 设置笔记的通话数据 ID。 + * @param id 通话数据 ID + */ + public void setCallDataId(long id) { + // 调用 NoteData 类的 setCallDataId 方法,设置通话数据 ID + mNoteData.setCallDataId(id); + } +/* + * 设置调用数据的方法。 + * 该方法接受两个参数:一个键和一个值,并将它们传递给 `mNoteData` 对象的 `setCallData` 方法。 + * @param key 用于存储数据的键。 + * @param value 与键关联的值。 + */ + public void setCallData(String key, String value) { + //调用 `mNoteData` 对象的 `setCallData` 方法,并将 `key` 和 `value` 作为参数传递。 + mNoteData.setCallData(key, value); + } +/* + * 检查是否有本地修改的方法。 + * 该方法返回一个布尔值,指示是否有本地修改。如果有本地修改,则返回 true;否则返回 false。 + * 本地修改的判断基于两个条件: + * 1. `mNoteDiffValues` 集合的大小是否大于 0。 + * 2. `mNoteData` 对象是否报告本地修改。 + * @return 如果有本地修改,则返回 true;否则返回 false。 + */ + public boolean isLocalModified() { + // 检查 `mNoteDiffValues` 集合的大小是否大于 0,或者 `mNoteData` 对象是否报告本地修改。 + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } +/* + * 同步笔记的方法。 + * 该方法用于将本地修改的笔记数据同步到内容提供者中。 + * @param context 应用程序上下文。 + * @param noteId 笔记的唯一标识符。 + * @return 如果同步成功,则返回 true;否则返回 false。 + * @throws IllegalArgumentException 如果 noteId 小于或等于 0,则抛出此异常。 + */ + public boolean syncNote(Context context, long noteId) { + /// 检查 noteId 是否有效,如果无效则抛出异常。 + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + // 如果没有本地修改,则直接返回 true。 + if (!isLocalModified()) { + return true; + } + + /** + * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and + * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the + * note data info + * 理论上,一旦数据发生变化,笔记应该在 {@link NoteColumns#LOCAL_MODIFIED} 和 + * {@link NoteColumns#MODIFIED_DATE} 上更新。为了数据安全,即使更新笔记失败,我们也会更新笔记数据信息。 + */ + // 尝试更新笔记数据。 + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + // 如果更新失败,记录错误日志。 + Log.e(TAG, "Update note error, should not happen"); + // Do not return, fall through + // 不返回,继续执行。 + } + // 清空 mNoteDiffValues 集合。 + mNoteDiffValues.clear(); + // 如果 mNoteData 有本地修改,并且推送数据到内容提供者失败,则返回 false。 + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; + } + // 如果所有操作成功,则返回 true。 + return true; + } +/* + * 笔记数据类,用于管理笔记的文本数据和通话数据。 + */ + private class NoteData { + private long mTextDataId;// 文本数据的唯一标识符 + + private ContentValues mTextDataValues;// 文本数据的 ContentValues 对象 + + private long mCallDataId; // 通话数据的唯一标识符 + + private ContentValues mCallDataValues; // 通话数据的 ContentValues 对象 + + private static final String TAG = "NoteData";// 日志标签 +/* + * 构造函数,初始化文本数据和通话数据的 ContentValues 对象,并将数据 ID 设置为 0。 + */ + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } +/* + * 检查是否有本地修改的方法。 + * @return 如果有本地修改,则返回 true;否则返回 false。 + */ + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } +/* + * 设置文本数据的唯一标识符。 + * 该方法用于设置文本数据的唯一标识符。如果传入的 id 小于或等于 0,则抛出 IllegalArgumentException 异常。 + * @param id 文本数据的唯一标识符。 + * @throws IllegalArgumentException 如果 id 小于或等于 0,则抛出此异常。 + */ + void setTextDataId(long id) { + // 检查传入的 id 是否有效,如果无效则抛出异常。 + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); + } + mTextDataId = id;// 设置文本数据的唯一标识符。 + } +/* + * 设置通话数据的唯一标识符。 + * 该方法用于设置通话数据的唯一标识符。如果传入的 id 小于或等于 0,则抛出 IllegalArgumentException 异常。 + * @param id 通话数据的唯一标识符。 + * @throws IllegalArgumentException 如果 id 小于或等于 0,则抛出此异常。 + */ + void setCallDataId(long id) { + // 检查传入的 id 是否有效,如果无效则抛出异常。 + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); + } + mCallDataId = id; // 设置通话数据的唯一标识符。 + } +/* + * 设置通话数据的方法。 + * 该方法用于将给定的键值对存储到通话数据的 ContentValues 对象中,并更新笔记的本地修改状态和修改日期。 + * @param key 通话数据的键。 + * @param value 通话数据的值。 + */ + void setCallData(String key, String value) { + // 将给定的键值对存储到通话数据的 ContentValues 对象中。 + mCallDataValues.put(key, value); + // 更新笔记的本地修改状态为 1,表示有本地修改。 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 更新笔记的修改日期为当前时间。 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } +/* + *设置文本数据的方法。 + * 该方法用于将给定的键值对存储到文本数据的 ContentValues 对象中,并更新笔记的本地修改状态和修改日期。 + * @param key 文本数据的键。 + * @param value 文本数据的值。 + */ + void setTextData(String key, String value) { + // 将给定的键值对存储到文本数据的 ContentValues 对象中。 + mTextDataValues.put(key, value); + // 更新笔记的本地修改状态为 1,表示有本地修改。 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 更新笔记的修改日期为当前时间。 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } +/* + * 将数据推送到内容提供者的方法。 + * 该方法用于将文本数据和通话数据推送到内容提供者中。如果文本数据或通话数据有修改,则将其插入或更新到内容提供者中。 + * @param context 应用程序上下文。 + * @param noteId 笔记的唯一标识符。 + * @return 如果推送成功,则返回 Uri;否则返回 null。 + * @throws IllegalArgumentException 如果 noteId 小于或等于 0,则抛出此异常。 + */ + Uri pushIntoContentResolver(Context context, long noteId) { + // 这是方法的签名,定义了方法的访问修饰符、返回类型、方法名称和参数列表。 + /** + * Check for safety + */ + //检查 noteId 是否有效,如果无效则抛出 + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + //创建一个操作列表,用于批量操作。 + ArrayList operationList = new ArrayList(); + ContentProviderOperation.Builder builder = null; + //如果文本数据有修改,处理文本数据。 + if(mTextDataValues.size() > 0) { + //将笔记 ID 添加到文本数据的 ContentValues 对象中。 + mTextDataValues.put(DataColumns.NOTE_ID, noteId); + //如果文本数据的 ID 为 0,表示是新数据,插入到内容提供者中。 + 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; + } + } //如果文本数据的 ID 不为 0,表示是已有数据,更新到内容提供者中。 + else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear();// 清空文本数据的 ContentValues 对象。 + } + // 如果通话数据有修改,处理通话数据。 + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); + //将笔记 ID 添加到通话数据的 ContentValues 对象中。 + //如果通话数据的 ID 为 0,表示是新数据,插入到内容提供者中。 + 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; + } + } //如果通话数据的 ID 不为 0,表示是已有数据,更新到内容提供者中。 + else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear();//清空通话数据的 ContentValues 对象。 + } + //如果操作列表中有操作,执行批量操作。 + 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 e) { + //捕获远程异常并记录日志。 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } catch (OperationApplicationException e) { + // 捕获操作应用异常并记录日志。 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} diff --git a/WorkingNote.java b/WorkingNote.java new file mode 100644 index 0000000..ebb8233 --- /dev/null +++ b/WorkingNote.java @@ -0,0 +1,532 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +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 { + // Note for the working note + // 当前工作笔记的 Note 对象 + private Note mNote; + // Note Id + // 笔记的唯一标识符 + private long mNoteId; + // Note content + // 笔记内容 + private String mContent; + // Note mode + // 笔记模式 + private int mMode; + // 提醒日期 + private long mAlertDate; + // 修改日期 + private long mModifiedDate; + // 背景颜色 ID + private int mBgColorId; + // 小部件 ID + private int mWidgetId; + // 小部件类型 + private int mWidgetType; + // 文件夹 ID + private long mFolderId; + // 应用程序上下文 + 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,// 数据的 MIME 类型 + DataColumns.DATA1,// 数据字段 1 + DataColumns.DATA2, // 数据字段 2 + DataColumns.DATA3, // 数据字段 3 + DataColumns.DATA4, // 数据字段 4 + }; +/* + * 笔记查询投影。 + * 该常量定义了在查询笔记时需要返回的列。 + * @see NoteColumns + */ + public static final String[] NOTE_PROJECTION = new String[] { + NoteColumns.PARENT_ID, // 笔记的父 ID(文件夹 ID) + NoteColumns.ALERTED_DATE, // 笔记的提醒日期 + NoteColumns.BG_COLOR_ID, // 笔记的背景颜色 ID + NoteColumns.WIDGET_ID, // 笔记的小部件 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; + + // New note construct + /* + * 创建一个新的空笔记。 + * 该构造函数用于初始化一个新的空笔记对象。 + * @param context 应用程序上下文。 + * @param folderId 文件夹 ID。 + */ + private WorkingNote(Context context, long folderId) { + // 设置应用程序上下文 + mContext = context; + mAlertDate = 0; // 初始化提醒日期为 0 + mModifiedDate = System.currentTimeMillis();// 设置修改日期为当前时间 + mFolderId = folderId;// 设置文件夹 ID + mNote = new Note();// 创建一个新的 Note 对象 + mNoteId = 0;// 初始化笔记 ID 为 0 + mIsDeleted = false;// 初始化删除状态为 false + mMode = 0;// 初始化笔记模式为 0 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE;// 初始化小部件类型为无效类型 + } + + // Existing note construct + /* + * 加载现有的笔记 + * 该构造函数用于加载现有的笔记对象。 + * @param context 应用程序上下文。 + * @param noteId 笔记的唯一标识符。 + * @param folderId 文件夹 ID。 + */ + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context;// 设置应用程序上下文 + mNoteId = noteId;// 设置笔记的唯一标识符 + mFolderId = folderId;// 设置文件夹 ID + mIsDeleted = false;// 初始化删除状态为 false + mNote = new Note(); // 创建一个新的 Note 对象 + loadNote();// 加载笔记数据 + } +/* + * 加载笔记数据。 + * 该方法用于从内容提供者中加载笔记数据,并将其设置到当前的 WorkingNote 对象中。 + */ + private void loadNote() { + // 查询笔记数据 + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); // 检查游标是否为空 + + if (cursor != null) { // 移动游标到第一行 + if (cursor.moveToFirst()) { + mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); // 获取文件夹 ID + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); // 获取背景颜色 ID + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);// 获取小部件 ID + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); // 获取小部件类型 + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);// 获取提醒日期 + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);// 获取修改日期 + } + cursor.close(); // 关闭游标 + } else { // 记录错误日志 + Log.e(TAG, "No note with id:" + mNoteId); + // 抛出异常 + throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + } + loadNoteData();// 加载笔记数据 + } +/* + * 加载笔记数据。 + * 该方法用于从内容提供者中加载笔记的具体数据,并将其设置到当前的 WorkingNote 对象中。 + */ + private void loadNoteData() {// 查询笔记数据 + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[] { + String.valueOf(mNoteId) + }, null); // 检查游标是否为空 + + if (cursor != null) { // 移动游标到第一行 + if (cursor.moveToFirst()) { + do { // 获取数据的 MIME 类型 + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + if (DataConstants.NOTE.equals(type)) {// 根据 MIME 类型处理数据 + mContent = cursor.getString(DATA_CONTENT_COLUMN); // 如果是文本笔记,获取内容和模式 + mMode = cursor.getInt(DATA_MODE_COLUMN); + mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + } else if (DataConstants.CALL_NOTE.equals(type)) { + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); // 如果是通话笔记,获取通话数据 ID + } else { + Log.d(TAG, "Wrong note type with type:" + type); // 记录错误日志 + } + } while (cursor.moveToNext());// 移动到下一行 + } + cursor.close(); // 关闭游标 + } else { + Log.e(TAG, "No data with id:" + mNoteId); // 记录错误日志 + // 抛出异常 + throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + } + } +/* + * 创建一个新的空笔记。 + * 该方法用于创建一个新的空笔记对象,并设置其背景颜色、小部件 ID 和小部件类型。 + * @param context 应用程序上下文。 + * @param folderId 文件夹 ID。 + * @param widgetId 小部件 ID。 + * @param widgetType 小部件类型。 + * @param defaultBgColorId 默认背景颜色 ID。 + * @return 新的 WorkingNote 对象。 + */ + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId);// 创建一个新的空笔记对象 + note.setBgColorId(defaultBgColorId); // 设置背景颜色 ID + note.setWidgetId(widgetId);// 设置小部件 ID + note.setWidgetType(widgetType); // 设置小部件类型 + return note;// 返回新的 WorkingNote 对象 + } +/* + *加载现有的笔记。 + * 该方法用于加载现有的笔记对象。 + * @param context 应用程序上下文。 + * @param id 笔记的唯一标识符。 + * @return 加载的 WorkingNote 对象。 + */ + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); // 返回一个新的 WorkingNote 对象,使用指定的上下文和笔记 ID + } +/* + * 保存笔记。 + * 该方法用于保存当前的笔记对象。如果笔记值得保存,则将其同步到数据库中,并更新小部件内容(如果存在小部件)。 + * @return 如果保存成功,则返回 true;否则返回 false。 + */ + public synchronized boolean saveNote() { + if (isWorthSaving()) { // 检查笔记是否值得保存 + if (!existInDatabase()) { // 如果笔记不存在于数据库中 + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + // 获取新的笔记 ID + Log.e(TAG, "Create new note fail with id:" + mNoteId);// 记录错误日志 + return false; + } + } + + mNote.syncNote(mContext, mNoteId);// 同步笔记到数据库 + + /** + * Update widget content if there exist any widget of this note + * 如果存在该笔记的小部件,更新小部件内容。 + */ + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } +/* + * 检查笔记是否存在于数据库中。 + * 该方法用于检查当前笔记对象是否已经存在于数据库中。 + * @return 如果笔记存在于数据库中,则返回 true;否则返回 false。 + */ + public boolean existInDatabase() {// 如果笔记 ID 大于 0,表示笔记存在于数据库中 + return mNoteId > 0; + } +/* + * 检查笔记是否值得保存。 + * 该方法用于检查当前笔记对象是否值得保存。如果笔记已被删除、内容为空且不存在于数据库中,或者笔记存在于数据库中但没有本地修改,则不值得保存。 + * @return 如果笔记值得保存,则返回 true;否则返回 false。 + */ + private boolean isWorthSaving() { + // 如果笔记已被删除,或者内容为空且不存在于数据库中,或者笔记存在于数据库中但没有本地修改,则不值得保存 + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())) { + return false; + } else { + return true; + } + } +/* + * 设置笔记设置状态监听器。 + * 该方法用于设置笔记设置状态的监听器,以便在笔记设置状态发生变化时通知监听器。 + * @param l 笔记设置状态监听器。 + */ + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; // 设置笔记设置状态监听器 + } +/* + * 设置提醒日期。 + * 该方法用于设置笔记的提醒日期。如果新的提醒日期与当前提醒日期不同,则更新提醒日期,并通知笔记设置状态监听器。 + * @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); // 通知笔记设置状态监听器提醒日期已更改 + } + } +/* + * 标记笔记为已删除或未删除。 + * 该方法用于标记笔记为已删除或未删除。如果笔记存在小部件,并且笔记设置状态监听器不为空,则通知笔记设置状态监听器小部件已更改。 + * @param mark 是否标记为已删除。 + */ + public void markDeleted(boolean mark) { + mIsDeleted = mark; // 标记笔记为已删除或未删除 + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID // 如果笔记存在小部件,并且笔记设置状态监听器不为空 + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + // 通知笔记设置状态监听器小部件已更改 + mNoteSettingStatusListener.onWidgetChanged(); + } + } +/* + * 设置背景颜色 ID。 + * 该方法用于设置笔记的背景颜色 ID。如果新的背景颜色 ID 与当前背景颜色 ID 不同,则更新背景颜色 ID,并通知笔记设置状态监听器背景颜色已更改。 + * @param id 背景颜色 ID。 + */ + public void setBgColorId(int id) { + if (id != mBgColorId) { // 如果新的背景颜色 ID 与当前背景颜色 ID 不同 + mBgColorId = id;// 更新背景颜色 ID + if (mNoteSettingStatusListener != null) { // 如果笔记设置状态监听器不为空 + mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知笔记设置状态监听器背景颜色已更改 + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 设置笔记的背景颜色 ID + } + } +/* + * 设置检查列表模式。 + * 该方法用于设置笔记的检查列表模式。如果新的模式与当前模式不同,则更新模式,并通知笔记设置状态监听器检查列表模式已更改。 + * @param mode 检查列表模式。 + */ + public void setCheckListMode(int mode) {// 如果新的模式与当前模式不同 + if (mMode != mode) { // 如果笔记设置状态监听器不为空 + if (mNoteSettingStatusListener != null) { // 通知笔记设置状态监听器检查列表模式已更改 + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + } + mMode = mode;// 更新模式 + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 设置笔记的检查列表模式 + } + } +/* + * 设置小部件类型。 + * 该方法用于设置笔记的小部件类型。如果新的类型与当前类型不同,则更新小部件类型,并将其保存到笔记数据中。 + * @param type 小部件类型。 + */ + public void setWidgetType(int type) { // 如果新的类型与当前类型不同 + if (type != mWidgetType) { // 更新小部件类型 + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 设置笔记的小部件类型 + } + } +/* + * 设置小部件 ID。 + * 该方法用于设置笔记的小部件 ID。如果新的 ID 与当前 ID 不同,则更新小部件 ID,并将其保存到笔记数据中。 + * @param id 小部件 ID。 + */ + public void setWidgetId(int id) { // 如果新的 ID 与当前 ID 不同 + if (id != mWidgetId) { // 更新小部件 ID + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 设置笔记的小部件 ID + } + } +/* + * 设置工作文本。 + * 该方法用于设置笔记的工作文本。如果新的文本与当前文本不同,则更新工作文本,并将其保存到笔记数据中。 + * @param text 工作文本。 + */ + public void setWorkingText(String text) { // 如果新的文本与当前文本不同 + if (!TextUtils.equals(mContent, text)) { // 更新工作文本 + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent);// 设置笔记的工作文本 + } + } +/* + * 将笔记转换为通话笔记。 + * 该方法用于将笔记转换为通话笔记。它设置通话日期、电话号码和父文件夹 ID。 + * @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)); // 设置父文件夹 ID + } +/* + * 检查是否有提醒。 + * 该方法用于检查笔记是否有提醒。如果提醒日期大于 0,则表示有提醒。 + * @return 如果有提醒,则返回 true;否则返回 false。 + */ + 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。 + * 该方法用于获取笔记的唯一标识符。 + * @return 笔记 ID。 + */ + public long getNoteId() { + return mNoteId; + } +/* + * 获取文件夹 ID。 + * 该方法用于获取笔记所属的文件夹 ID。 + * @return 文件夹 ID。 + */ + public long getFolderId() { + return mFolderId; + } +/* + * 获取小部件 ID。 + * 该方法用于获取笔记的小部件 ID。 + * @return 小部件 ID。 + */ + public int getWidgetId() { + return mWidgetId; + } +/* + * 获取小部件类型。 + * 该方法用于获取笔记的小部件类型。 + * @return 小部件类型。 + */ + public int getWidgetType() { + return mWidgetType; + } +/* + * 笔记设置状态监听器接口。 + * 该接口定义了笔记设置状态发生变化时的回调方法。 + */ + public interface NoteSettingChangedListener { + /** + * Called when the background color of current note has just changed + * 当当前笔记的背景颜色发生变化时调用。 + */ + void onBackgroundColorChanged(); + + /** + * Called when user set clock + * 当用户设置提醒时调用。 + * @param date 提醒日期。 + * @param set 是否设置提醒 + */ + void onClockAlertChanged(long date, boolean set); + + /** + * Call when user create note from widget + * 当用户从小部件创建笔记时调用。 + */ + void onWidgetChanged(); + + /** + * Call when switch between check list mode and normal mode + * @param oldMode is previous mode before change + * @param newMode is new mode + * 当用户在检查列表模式和普通模式之间切换时调用。 + * @param oldMode 之前的模式。 + * @param newMode 新的模式。 + */ + void onCheckListModeChanged(int oldMode, int newMode); + } +} -- 2.34.1 From 868181ef52b298b790d87ba4652912f26802191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:39:47 +0800 Subject: [PATCH 08/44] Signed-off-by: --- Contact.txt | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Contact.txt diff --git a/Contact.txt b/Contact.txt new file mode 100644 index 0000000..91b2e7e --- /dev/null +++ b/Contact.txt @@ -0,0 +1,81 @@ +/* + * 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.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 { //缂撳瓨鑱旂郴浜轰俊鎭殑HashMap锛岄敭涓虹數璇濆彿鐮侊紝鍊间负鑱旂郴浜哄鍚 + private static HashMap sContactCache; //鏃ュ織鏍囩 + private static final String TAG = "Contact"; //鏌ヨ鑱旂郴浜轰俊鎭殑SQL閫夋嫨璇彞妯℃澘 + + 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); + } + //鏇挎崲SQL閫夋嫨璇彞涓殑鍗犱綅绗 + 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; + } + } +} -- 2.34.1 From 7bb15e21f4eb78562c8f98a60bdadcafff2a9472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:43:02 +0800 Subject: [PATCH 09/44] Signed-off-by: --- Notes.txt | 441 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 Notes.txt diff --git a/Notes.txt b/Notes.txt new file mode 100644 index 0000000..6bff438 --- /dev/null +++ b/Notes.txt @@ -0,0 +1,441 @@ +/* + * 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; +/* + * 瀹氫箟浜嗕笌绗旇鐩稿叧鐨勫父閲忓拰鎺ュ彛 + */ +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; //绯荤粺鏂囦欢澶 + + /** + * Following IDs are system folders' identifiers + * {@link Notes#ID_ROOT_FOLDER } is default folder + * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder + * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + */ + /* + * 绯荤粺鏂囦欢澶圭殑鏍囪瘑绗 + * {@link Notes#ID_ROOT_FOLDER }鏄粯璁ゆ枃浠跺す + * {@link Notes#ID_TEMPARAY_FOLDER }鏄敤浜庢病鏈夋墍灞炴枃浠跺す鐨勭瑪璁 + * {@link Notes#ID_CALL_RECORD_FOLDER}鏄敤浜庡瓨鍌ㄩ氳瘽璁板綍鐨勬枃浠跺す + */ + public static final int ID_ROOT_FOLDER = 0; + public static final int ID_TEMPARAY_FOLDER = -1; + public static final int ID_CALL_RECORD_FOLDER = -2; + public static final int ID_TRASH_FOLER = -3; + //鐢ㄤ簬Intent浼犻掔殑棰濆鏁版嵁閿 + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_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"; + 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 to query all notes and folders + */ + /* + * Uri鐢ㄤ簬鏌ヨ鎵鏈夌殑绗旇鍜屾枃浠跺す + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * Uri to query data + */ + /* + * Uri鐢ㄤ簬鏌ヨ鏁版嵁 + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + /* + * 绗旇琛ㄧ殑鍒楀畾涔 + */ + public interface NoteColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + /* + * 琛岀殑鍞竴ID + *

绫诲瀷锛欼NTEGER(long)

+ */ + public static final String ID = "_id"; + + /** + * The parent's id for note or folder + *

Type: INTEGER (long)

+ */ + /* + * 绗旇鎴栨枃浠跺す鐨勭埗ID + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + /* + * 鍒涘缓鏃ユ湡 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + /* + * 鏈鍚庝慨鏀规棩鏈 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + + /** + * Alert date + *

Type: INTEGER (long)

+ */ + /* + * 鎻愰啋鏃ユ湡 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * Folder's name or text content of note + *

Type: TEXT

+ */ + /* + * 鏂囦欢澶圭殑鍚嶇О鎴栫瑪璁扮殑鏂囨湰鍐呭 + *

绫诲瀷锛歍EXT

+ */ + public static final String SNIPPET = "snippet"; + + /** + * Note's widget id + *

Type: INTEGER (long)

+ */ + /* + * 绗旇鐨勫皬閮ㄤ欢ID + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * Note's widget type + *

Type: INTEGER (long)

+ */ + /* + * 绗旇鐨勫皬閮ㄤ欢绫诲瀷 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * Note's background color's id + *

Type: INTEGER (long)

+ */ + /* + * 绗旇鐨勮儗鏅鑹睮D + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * For text note, it doesn't has attachment, for multi-media + * note, it has at least one attachment + *

Type: INTEGER

+ */ + /* + * 鏄惁鍖呭惈缁勪欢 + *

绫诲瀷锛欼NTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * Folder's count of notes + *

Type: INTEGER (long)

+ */ + /* + * 鏂囦欢澶逛腑鐨勭瑪璁版暟閲 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * The file type: folder or note + *

Type: INTEGER

+ */ + /* + * 鏂囦欢绫诲瀷锛氭枃浠跺す鎴栫瑪璁 + *

绫诲瀷锛欼NTEGER

+ */ + public static final String TYPE = "type"; + + /** + * The last sync id + *

Type: INTEGER (long)

+ */ + /* + * 鏈鍚庝竴娆″悓姝ョ殑ID + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * Sign to indicate local modified or not + *

Type: INTEGER

+ */ + /* + * 鎸囩ず鏄惁鍦ㄦ湰鍦拌淇敼 + *

绫诲瀷锛欼NTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * Original parent id before moving into temporary folder + *

Type : INTEGER

+ */ + /* + * 绉诲姩鍒颁复鏃舵枃浠跺す涔嬪墠鐨勫師濮嬬埗ID + *

绫诲瀷锛欼NTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * The gtask id + *

Type : TEXT

+ */ + /* + * gtask ID + *

绫诲瀷锛歍EXT

+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * The version code + *

Type : INTEGER (long)

+ */ + /* + * 鐗堟湰鍙 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String VERSION = "version"; + } + /* + * 鏁版嵁琛ㄧ殑鍒楀畾涔 + */ + public interface DataColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + /* + * 琛岀殑鍞竴ID + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The MIME type of the item represented by this row. + *

Type: Text

+ */ + /* + * 璇ヨ鐨凪IME绫诲瀷 + *

绫诲瀷锛歍EXT

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * The reference id to note that this data belongs to + *

Type: INTEGER (long)

+ */ + /* + * 璇ユ暟鎹墍灞炵殑绗旇ID + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + /* + * 鍒涘缓鏃ユ湡 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + /* + * 鏈鍚庝慨鏀规棩鏈 + *

绫诲瀷锛欼NTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * Data's content + *

Type: TEXT

+ */ + /* + * 鏁版嵁鍐呭 + *

绫诲瀷锛歍EXT

+ */ + public static final String CONTENT = "content"; + + + /** + * Generic data column, the meaning is{@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + /* + * 閫氱敤鏁版嵁鍒楋紝鍏蜂綋鍚箟鐢眥@link #MIMETYPE}鍐冲畾锛岀敤浜庢暣鏁版暟鎹被鍨 + *

绫诲瀷锛欼NTEGER

+ */ + public static final String DATA1 = "data1"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + /* + * 閫氱敤鏁版嵁鍒楋紝鍏蜂綋鍚箟鐢眥@link #MIMETYPE}鍐冲畾锛岀敤浜庢暣鏁版暟鎹被鍨 + *

绫诲瀷: INTEGER

+ */ + public static final String DATA2 = "data2"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + /* + * 閫氱敤鏁版嵁鍒楋紝鍏蜂綋鍚箟鐢眥@link #MIMETYPE}鍐冲畾锛岀敤浜嶵EXT鏁版嵁绫诲瀷 + *

绫诲瀷: TEXT

+ */ + public static final String DATA3 = "data3"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + /* + * 閫氱敤鏁版嵁鍒楋紝鍏蜂綋鍚箟鐢眥@link #MIMETYPE}鍐冲畾锛岀敤浜嶵EXT鏁版嵁绫诲瀷 + *

绫诲瀷: TEXT

+ */ + public static final String DATA4 = "data4"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + /* + * 閫氱敤鏁版嵁鍒楋紝鍏蜂綋鍚箟鐢眥@link #MIMETYPE}鍐冲畾锛岀敤浜嶵EXT鏁版嵁绫诲瀷 + *

绫诲瀷: TEXT

+ */ + public static final String DATA5 = "data5"; + } + /* + * 鏂囨湰绗旇鐨勫畾涔 + */ + public static final class TextNote implements DataColumns { + /** + * Mode to indicate the text in check list mode or not + *

Type: Integer 1:check list mode 0: normal mode

+ */ + /* + * 妯″紡锛屾寚绀烘枃鏈槸鍚﹀湪妫鏌ュ垪琛ㄦā寮 + *

绫诲瀷: INTEGER 1:妫鏌ュ垪琛ㄦā寮 0: 姝e父妯″紡

+ */ + + 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"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + } + /* + * 閫氳瘽绗旇鐨勫畾涔 + */ + public static final class CallNote implements DataColumns { + /** + * Call date for this record + *

Type: INTEGER (long)

+ */ + /* + * 閫氳瘽鏃ユ湡 + *

绫诲瀷: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; + + /** + * Phone number for this record + *

Type: TEXT

+ */ + /* + * 鐢佃瘽鍙风爜 + *

绫诲瀷: TEXT

+ */ + + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + } +} + +/* + * 鐗堟潈澹版槑锛氫繚鐣欎簡鍘熸湁鐨勭増鏉冨0鏄庯紝璇存槑浠g爜鐨勮鍙崗璁拰鐗堟潈淇℃伅銆 + + 甯搁噺瀹氫箟锛氳В閲婁簡鍚勪釜甯搁噺鐨勭敤閫旓紝濡傜瑪璁扮被鍨嬨佺郴缁熸枃浠跺すID銆両ntent浼犻掔殑棰濆鏁版嵁閿瓑銆 + + 鎺ュ彛瀹氫箟锛氳缁嗚鏄庝簡鍚勪釜鎺ュ彛涓殑鍒楀畾涔夛紝瑙i噴浜嗘瘡涓垪鐨勭敤閫斿拰鏁版嵁绫诲瀷銆 + + 绫诲畾涔夛細瑙i噴浜 TextNote 鍜 CallNote 绫荤殑鐢ㄩ旓紝骞惰缁嗚鏄庝簡瀹冧滑鐨勬暟鎹垪鍜屽父閲忋 + */ -- 2.34.1 From c80c75816a401033ed2c80af4c066e0dcdfffa06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:48:45 +0800 Subject: [PATCH 10/44] Signed-off-by: --- NotesDatabaseHelper.txt | 475 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 NotesDatabaseHelper.txt diff --git a/NotesDatabaseHelper.txt b/NotesDatabaseHelper.txt new file mode 100644 index 0000000..5900a61 --- /dev/null +++ b/NotesDatabaseHelper.txt @@ -0,0 +1,475 @@ +/* + * 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.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; + +/* + * NotesDatabaseHelper 绫荤户鎵胯嚜 SQLiteOpenHelper锛岀敤浜庣鐞 SQLite 鏁版嵁搴撶殑鍒涘缓鍜岀増鏈崌绾с + * 璇ョ被璐熻矗鍒涘缓鍜岀鐞嗕袱涓富瑕佺殑鏁版嵁搴撹〃锛歂OTE 琛ㄥ拰 DATA 琛紝浠ュ強鐩稿叧鐨勮Е鍙戝櫒鍜岀储寮曘 + */ +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 璇彞 + /* + * 鍒涘缓绗旇琛ㄧ殑 SQL 璇彞銆 + * 璇ヨ〃鐢ㄤ簬瀛樺偍绗旇鐨勭浉鍏充俊鎭紝鍖呮嫭绗旇鐨 ID銆佺埗 ID銆佹彁閱掓棩鏈熴佽儗鏅鑹 ID銆佸垱寤烘棩鏈熴 + * 鏄惁鏈夐檮浠躲佷慨鏀规棩鏈熴佺瑪璁版暟閲忋佹憳瑕併佺被鍨嬨佸皬閮ㄤ欢 ID銆佸皬閮ㄤ欢绫诲瀷銆佸悓姝 ID銆佹湰鍦颁慨鏀规爣蹇椼 + * 鍘熷鐖 ID銆丟oogle 浠诲姟 ID 鍜岀増鏈彿銆 + */ + 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," + // 鐖剁瑪璁扮殑 ID锛岄粯璁や负 0 + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 鎻愰啋鏃ユ湡锛岄粯璁や负 0 + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 鑳屾櫙棰滆壊 ID锛岄粯璁や负 0 + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 鍒涘缓鏃ユ湡锛岄粯璁や负褰撳墠鏃堕棿 + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + // 鏄惁鏈夐檮浠讹紝榛樿涓 0 + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 淇敼鏃ユ湡锛岄粯璁や负褰撳墠鏃堕棿 + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 绗旇鏁伴噺锛岄粯璁や负 0 + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 绗旇鎽樿锛岄粯璁や负绌哄瓧绗︿覆 + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 绗旇绫诲瀷锛岄粯璁や负 0 + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + // 灏忛儴浠 ID锛岄粯璁や负 0 + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 灏忛儴浠剁被鍨嬶紝榛樿涓 -1 + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 鍚屾 ID锛岄粯璁や负 0 + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 鏈湴淇敼鏍囧織锛岄粯璁や负 0 + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 鍘熷鐖 ID锛岄粯璁や负 0 + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // Google 浠诲姟 ID锛岄粯璁や负绌哄瓧绗︿覆 + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 鐗堟湰鍙凤紝榛樿涓 0 + ")"; + // 鍒涘缓 DATA 琛ㄧ殑 SQL 璇彞 + /* + * 鍒涘缓鏁版嵁琛ㄧ殑 SQL 璇彞銆 + * 璇ヨ〃鐢ㄤ簬瀛樺偍涓庣瑪璁扮浉鍏崇殑鏁版嵁锛屽寘鎷暟鎹殑 ID銆丮IME 绫诲瀷銆佹墍灞炵瑪璁扮殑 ID銆佸垱寤烘棩鏈熴 + * 淇敼鏃ユ湡銆佸唴瀹广佷互鍙婁竴浜涢澶栫殑鏁版嵁瀛楁锛圖ATA1 鍒 DATA5锛夈 + */ + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + // 鏁版嵁鐨勫敮涓鏍囪瘑绗︼紝涓婚敭 + DataColumns.MIME_TYPE + " TEXT NOT NULL," + // MIME 绫诲瀷锛屼笉鑳戒负绌 + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 鎵灞炵瑪璁扮殑 ID锛岄粯璁や负 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," + // 棰濆鏁版嵁瀛楁 1锛屾暣鏁扮被鍨 + DataColumns.DATA2 + " INTEGER," + // 棰濆鏁版嵁瀛楁 2锛屾暣鏁扮被鍨 + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 棰濆鏁版嵁瀛楁 3锛屾枃鏈被鍨嬶紝榛樿涓虹┖瀛楃涓 + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 棰濆鏁版嵁瀛楁 4锛屾枃鏈被鍨嬶紝榛樿涓虹┖瀛楃涓 + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 棰濆鏁版嵁瀛楁 5锛屾枃鏈被鍨嬶紝榛樿涓虹┖瀛楃涓 + ")"; + // 鍒涘缓 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 + ");"; + // 褰撶瑪璁扮Щ鍔ㄥ埌鏂囦欢澶规椂锛屽鍔犳枃浠跺す鐨勭瑪璁拌鏁拌Е鍙戝櫒 + /** + * Increase folder's note count when move note to the folder + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鏇存柊绗旇鐨勭埗 ID 鏃跺鍔犵埗鏂囦欢澶圭殑绗旇鏁伴噺銆 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鐨 PARENT_ID 瀛楁鏇存柊鍚庤Е鍙戯紝骞跺皢鐖舵枃浠跺す鐨 NOTES_COUNT 瀛楁鍔 1銆 + */ + 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"; + // 褰撶瑪璁颁粠鏂囦欢澶逛腑绉诲嚭鏃讹紝鍑忓皯鏂囦欢澶圭殑绗旇璁℃暟瑙﹀彂鍣 + /** + * Decrease folder's note count when move note from folder + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鏇存柊绗旇鐨勭埗 ID 鏃跺噺灏戞棫鐖舵枃浠跺す鐨勭瑪璁版暟閲忋 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鐨 PARENT_ID 瀛楁鏇存柊鍚庤Е鍙戯紝骞跺皢鏃х埗鏂囦欢澶圭殑 NOTES_COUNT 瀛楁鍑 1銆 + */ + 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"; + // 褰撴彃鍏ユ柊绗旇鍒版枃浠跺す鏃讹紝澧炲姞鏂囦欢澶圭殑绗旇璁℃暟瑙﹀彂鍣 + /** + * Increase folder's note count when insert new note to the folder + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鎻掑叆鏂扮瑪璁版椂澧炲姞鐖舵枃浠跺す鐨勭瑪璁版暟閲忋 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鎻掑叆鏂拌褰曞悗瑙﹀彂锛屽苟灏嗙埗鏂囦欢澶圭殑 NOTES_COUNT 瀛楁鍔 1銆 + */ + 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"; + // 褰撲粠鏂囦欢澶逛腑鍒犻櫎绗旇鏃讹紝鍑忓皯鏂囦欢澶圭殑绗旇璁℃暟瑙﹀彂鍣 + /** + * Decrease folder's note count when delete note from the folder + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鍒犻櫎绗旇鏃跺噺灏戠埗鏂囦欢澶圭殑绗旇鏁伴噺銆 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鍒犻櫎璁板綍鍚庤Е鍙戯紝骞跺皢鐖舵枃浠跺す鐨 NOTES_COUNT 瀛楁鍑 1銆 + */ + 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"; + // 褰撴彃鍏ョ被鍨嬩负 {@link DataConstants#NOTE} 鐨勬暟鎹椂锛屾洿鏂扮瑪璁板唴瀹硅Е鍙戝櫒 + /** + * Update note's content when insert data with type {@link DataConstants#NOTE} + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鎻掑叆鏂版暟鎹椂鏇存柊绗旇鐨勫唴瀹规憳瑕併 + * 璇ヨЕ鍙戝櫒鍦ㄦ暟鎹〃鎻掑叆鏂拌褰曞悗瑙﹀彂锛屽苟涓斾粎鍦ㄦ彃鍏ョ殑鏁版嵁 MIME 绫诲瀷涓 'NOTE' 鏃舵墽琛屻 + * 瑙﹀彂鍣ㄤ細灏嗙瑪璁拌〃涓搴旂瑪璁扮殑 SNIPPET 瀛楁鏇存柊涓烘柊鎻掑叆鏁版嵁鐨勫唴瀹广 + */ + 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"; + // 褰撶被鍨嬩负 {@link DataConstants#NOTE} 鐨勬暟鎹洿鏂版椂锛屾洿鏂扮瑪璁板唴瀹硅Е鍙戝櫒 + /** + * Update note's content when data with {@link DataConstants#NOTE} type has changed + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鏇存柊鏁版嵁鏃舵洿鏂扮瑪璁扮殑鍐呭鎽樿銆 + * 璇ヨЕ鍙戝櫒鍦ㄦ暟鎹〃鏇存柊璁板綍鍚庤Е鍙戯紝骞朵笖浠呭湪鏇存柊鐨勬暟鎹 MIME 绫诲瀷涓 'NOTE' 鏃舵墽琛屻 + * 瑙﹀彂鍣ㄤ細灏嗙瑪璁拌〃涓搴旂瑪璁扮殑 SNIPPET 瀛楁鏇存柊涓烘洿鏂板悗鐨勬暟鎹唴瀹广 + */ + 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"; + // 褰撶被鍨嬩负 {@link DataConstants#NOTE} 鐨勬暟鎹垹闄ゆ椂锛屾洿鏂扮瑪璁板唴瀹硅Е鍙戝櫒 + /** + * Update note's content when data with {@link DataConstants#NOTE} type has deleted + */ + /* + *鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鍒犻櫎鏁版嵁鏃舵洿鏂扮瑪璁扮殑鍐呭鎽樿銆 + * 璇ヨЕ鍙戝櫒鍦ㄦ暟鎹〃鍒犻櫎璁板綍鍚庤Е鍙戯紝骞朵笖浠呭湪鍒犻櫎鐨勬暟鎹 MIME 绫诲瀷涓 'NOTE' 鏃舵墽琛屻 + * 瑙﹀彂鍣ㄤ細灏嗙瑪璁拌〃涓搴旂瑪璁扮殑 SNIPPET 瀛楁娓呯┖銆 + */ + 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"; + // 褰撳垹闄ょ瑪璁版椂锛屽垹闄ょ浉鍏虫暟鎹Е鍙戝櫒 + /** + * Delete datas belong to note which has been deleted + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鍒犻櫎绗旇鏃跺垹闄や笌涔嬪叧鑱旂殑鏁版嵁銆 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鍒犻櫎璁板綍鍚庤Е鍙戯紝骞跺皢鏁版嵁琛ㄤ腑涓庤鍒犻櫎绗旇鍏宠仈鐨勬墍鏈夋暟鎹垹闄ゃ + */ + 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"; + // 褰撳垹闄ゆ枃浠跺す鏃讹紝鍒犻櫎鏂囦欢澶逛腑鐨勭瑪璁拌Е鍙戝櫒 + /** + * Delete notes belong to folder which has been deleted + */ + /* + *鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪鍒犻櫎绗旇鏃跺垹闄や笌涔嬪叧鑱旂殑鏁版嵁銆 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鍒犻櫎璁板綍鍚庤Е鍙戯紝骞跺皢鏁版嵁琛ㄤ腑涓庤鍒犻櫎绗旇鍏宠仈鐨勬墍鏈夋暟鎹垹闄ゃ + */ + 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"; + // 褰撴枃浠跺す绉诲姩鍒板洖鏀剁珯鏃讹紝绉诲姩鏂囦欢澶逛腑鐨勭瑪璁板埌鍥炴敹绔欒Е鍙戝櫒 + /** + * Move notes belong to folder which has been moved to trash folder + */ + /* + * 鍒涘缓涓涓Е鍙戝櫒锛岀敤浜庡湪灏嗘枃浠跺す绉诲姩鍒板洖鏀剁珯鏃讹紝灏嗗叾鎵鏈夊瓙绗旇涔熺Щ鍔ㄥ埌鍥炴敹绔欍 + * 璇ヨЕ鍙戝櫒鍦ㄧ瑪璁拌〃鏇存柊璁板綍鍚庤Е鍙戯紝骞朵笖浠呭湪鏇存柊鐨勭瑪璁扮殑 PARENT_ID 瀛楁鍊间负鍥炴敹绔欐枃浠跺す ID 鏃舵墽琛屻 + * 瑙﹀彂鍣ㄤ細灏嗘墍鏈夌埗 ID 涓鸿鏇存柊绗旇 ID 鐨勭瑪璁扮殑 PARENT_ID 瀛楁鍊兼洿鏂颁负鍥炴敹绔欐枃浠跺す ID銆 + */ + 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"; +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栨暟鎹簱 + * 璇ユ瀯閫犲嚱鏁扮敤浜庡垵濮嬪寲鏁版嵁搴撳府鍔╃被锛屽苟鎸囧畾鏁版嵁搴撳悕绉板拰鐗堟湰鍙枫 + * @param context 涓婁笅鏂 + */ + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } +/* + * 鍒涘缓 NOTE 琛 + * 鍒涘缓绗旇琛ㄥ強鍏剁浉鍏宠Е鍙戝櫒鍜岀郴缁熸枃浠跺す銆 + * 璇ユ柟娉曠敤浜庡湪鏁版嵁搴撲腑鍒涘缓绗旇琛紝骞惰皟鐢ㄧ浉鍏虫柟娉曞垱寤鸿Е鍙戝櫒鍜岀郴缁熸枃浠跺す銆 + * @param db SQLiteDatabase 瀹炰緥 + */ + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); // 鎵ц鍒涘缓绗旇琛ㄧ殑 SQL 璇彞 + reCreateNoteTableTriggers(db); // 閲嶆柊鍒涘缓绗旇琛ㄧ殑瑙﹀彂鍣 + createSystemFolder(db); // 鍒涘缓绯荤粺鏂囦欢澶 + Log.d(TAG, "note table has been created"); // 璁板綍鏃ュ織锛岃〃绀虹瑪璁拌〃宸插垱寤 + } +/* + * 閲嶆柊鍒涘缓 NOTE 琛ㄧ殑瑙﹀彂鍣 + * 璇ユ柟娉曠敤浜庡垹闄ょ幇鏈夌殑绗旇琛ㄨЕ鍙戝櫒锛屽苟閲嶆柊鍒涘缓瀹冧滑锛屼互纭繚瑙﹀彂鍣ㄤ笌琛ㄧ粨鏋勪竴鑷淬 + * @param 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); + } +/* + * 鍒涘缓绯荤粺鏂囦欢澶 + * 璇ユ柟娉曠敤浜庡湪鏁版嵁搴撲腑鍒涘缓绯荤粺鏂囦欢澶癸紝鍖呮嫭閫氳瘽璁板綍鏂囦欢澶广佹牴鏂囦欢澶广佷复鏃舵枃浠跺す鍜屽洖鏀剁珯鏂囦欢澶广 + * @param db SQLiteDatabase 瀹炰緥 + */ + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + // 鍒涘缓閫氳瘽璁板綍鏂囦欢澶, 閫氳瘽璁板綍鏂囦欢澶癸紝鐢ㄤ簬瀛樺偍閫氳瘽绗旇銆 + /** + * call record foler for call notes + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 鍒涘缓鏍规枃浠跺す锛堥粯璁ゆ枃浠跺す锛 + /** + * root folder which is default folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 鍒涘缓涓存椂鏂囦欢澶癸紙鐢ㄤ簬绉诲姩绗旇锛 + /** + * temporary folder which is used for moving note + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + // 鍒涘缓鍥炴敹绔欐枃浠跺す + /** + * create trash folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } +/* + * 鍒涘缓 DATA 琛 + * 璇ユ柟娉曠敤浜庡湪鏁版嵁搴撲腑鍒涘缓鏁版嵁琛紝骞惰皟鐢ㄧ浉鍏虫柟娉曞垱寤鸿Е鍙戝櫒鍜岀储寮曘 + * @param db SQLiteDatabase 瀹炰緥 + */ + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); // 鎵ц鍒涘缓鏁版嵁琛ㄧ殑 SQL 璇彞 + reCreateDataTableTriggers(db); // 閲嶆柊鍒涘缓鏁版嵁琛ㄧ殑瑙﹀彂鍣 + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); // 鍒涘缓鏁版嵁琛ㄧ殑绱㈠紩 + Log.d(TAG, "data table has been created"); // 璁板綍鏃ュ織锛岃〃绀烘暟鎹〃宸插垱寤 + } +/* + * 閲嶆柊鍒涘缓 DATA 琛ㄧ殑瑙﹀彂鍣 + * 璇ユ柟娉曠敤浜庡垹闄ょ幇鏈夌殑鏁版嵁琛ㄨЕ鍙戝櫒锛屽苟閲嶆柊鍒涘缓瀹冧滑锛屼互纭繚瑙﹀彂鍣ㄤ笌琛ㄧ粨鏋勪竴鑷淬 + * @param 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 鐨勫崟渚嬪疄渚 + * 璇ユ柟娉曚娇鐢 synchronized 鍏抽敭瀛楃‘淇濈嚎绋嬪畨鍏紝骞跺湪瀹炰緥涓嶅瓨鍦ㄦ椂鍒涘缓涓涓柊鐨勫疄渚嬨 + * @param context 涓婁笅鏂 + * @return 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 鏃х増鏈彿 + * @param newVersion 鏂扮増鏈彿 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + // 濡傛灉鏃х増鏈槸 1锛屾墽琛屼粠鐗堟湰 1 鍗囩骇鍒扮増鏈 2 鐨勬搷浣 + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // // 杩欎釜鍗囩骇鍖呮嫭浠 v2 鍒 v3 鐨勫崌绾 + oldVersion++; + } + // 濡傛灉鏃х増鏈槸 2锛屽苟涓旀病鏈夎烦杩 v2 鍗囩骇锛屾墽琛屼粠鐗堟湰 2 鍗囩骇鍒扮増鏈 3 鐨勬搷浣 + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + // 濡傛灉鏃х増鏈槸 3锛屾墽琛屼粠鐗堟湰 3 鍗囩骇鍒扮増鏈 4 鐨勬搷浣 + 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"); + } + } +/* + * 鍗囩骇鏁版嵁搴撳埌鐗堟湰 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); // 閲嶆柊鍒涘缓鏁版嵁琛ㄥ強鍏剁浉鍏宠Е鍙戝櫒鍜岀储寮 + } +/* + * 鍗囩骇鏁版嵁搴撳埌鐗堟湰 3 + * @param db SQLiteDatabase 瀹炰緥 + */ + private void upgradeToV3(SQLiteDatabase db) { + // drop unused triggers + // 鍒犻櫎涓嶅啀浣跨敤鐨勮Е鍙戝櫒 + 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"); + // add a column for gtask id + //涓 gtask id 娣诲姞鍒 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // add a trash system folder + // 娣诲姞鍥炴敹绔欑郴缁熸枃浠跺す + 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); + } +/* + *鍗囩骇鏁版嵁搴撳埌鐗堟湰 4 + * @param db SQLiteDatabase 瀹炰緥 + */ + private void upgradeToV4(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} -- 2.34.1 From b9cadba7b9eaa18b64fa1f05860e6e8d1cf2f9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:52:06 +0800 Subject: [PATCH 11/44] Signed-off-by: --- NotesProvider.txt | 375 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 NotesProvider.txt diff --git a/NotesProvider.txt b/NotesProvider.txt new file mode 100644 index 0000000..1688aa4 --- /dev/null +++ b/NotesProvider.txt @@ -0,0 +1,375 @@ +/* + * 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.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; + +/* + * NotesProvider 鏄竴涓 ContentProvider锛岀敤浜庣鐞嗙瑪璁版暟鎹殑澧炲垹鏀规煡鎿嶄綔銆 + * 瀹冮氳繃 UriMatcher 鏉ュ尮閰嶄笉鍚岀殑 URI锛屽苟鏍规嵁鍖归厤缁撴灉鎵ц鐩稿簲鐨勬暟鎹簱鎿嶄綔銆 + */ +public class NotesProvider extends ContentProvider { + private static final UriMatcher mMatcher; + // UriMatcher 鐢ㄤ簬鍖归厤涓嶅悓鐨 URI + 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; // 鍖归厤鎼滅储寤鸿 + // URI 鍖归厤鐮 + static { // 鍒濆鍖 UriMatcher + 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); + } +/* + * 鎼滅储缁撴灉鐨勬姇褰卞瓧娈碉紝鐢ㄤ簬鏋勫缓鎼滅储鏌ヨ銆 + *x'0A' 琛ㄧず SQLite 涓殑鎹㈣绗 '\n'銆 + * 鍦ㄦ悳绱㈢粨鏋滀腑锛屾爣棰樺拰鍐呭浼氬幓闄ゆ崲琛岀鍜岀┖鐧藉瓧绗︼紝浠ヤ究鏄剧ず鏇村淇℃伅銆 + */ + /** + * x'0A' represents the '\n' character in sqlite. For title and content in the search result, + * we will trim '\n' and white space in order to show more information. + */ + 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 鏌ヨ璇彞銆 + * 鏌ヨ鏉′欢鍖呮嫭锛氱瑪璁版憳瑕佸寘鍚悳绱㈠叧閿瓧銆佺埗 ID 涓嶆槸鍥炴敹绔欐枃浠跺す銆佺瑪璁扮被鍨嬩负鏅氱瑪璁般 + */ + 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; // 杩斿洖 true 琛ㄧず鍒濆鍖栨垚鍔 + } +/* + * 鏌ヨ鎿嶄綔銆 + * 璇ユ柟娉曟牴鎹紶鍏ョ殑 URI 鎵ц鐩稿簲鐨勬煡璇㈡搷浣滐紝骞惰繑鍥炴煡璇㈢粨鏋滅殑 Cursor銆 + * @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; + 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); // 鑾峰彇 URI 涓殑绗旇 ID + 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); // 鑾峰彇 URI 涓殑鏁版嵁 ID + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); // 鏌ヨ鍗曚釜鏁版嵁椤 + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); // 鑾峰彇鎼滅储寤鸿鐨勬悳绱㈠瓧绗︿覆 + } + } else { + searchString = uri.getQueryParameter("pattern"); // 鑾峰彇鎼滅储鐨勬悳绱㈠瓧绗︿覆 + } + + 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); // 璁剧疆 Cursor 鐨勯氱煡 URI + } + return c; // 杩斿洖鏌ヨ缁撴灉鐨 Cursor + } +/* + * 鎻掑叆鎿嶄綔銆 + * 璇ユ柟娉曟牴鎹紶鍏ョ殑 URI 鎵ц鐩稿簲鐨勬彃鍏ユ搷浣滐紝骞惰繑鍥炴彃鍏ュ悗鐨 URI銆 + * @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); // 鑾峰彇绗旇 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 寮傚父 + } + // Notify the note uri + // 閫氱煡绗旇 URI 鍙樺寲 + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // Notify the data uri + // 閫氱煡鏁版嵁 URI 鍙樺寲 + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + return ContentUris.withAppendedId(uri, insertedId); // 杩斿洖鎻掑叆鍚庣殑 URI + } +/* + * 鍒犻櫎鎿嶄綔銆 + * 璇ユ柟娉曟牴鎹紶鍏ョ殑 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 "; // 娣诲姞鍒犻櫎鏉′欢锛孖D 澶т簬 0 + count = db.delete(TABLE.NOTE, selection, selectionArgs); // 鍒犻櫎绗旇琛ㄤ腑鐨勬暟鎹 + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); // 鑾峰彇 URI 涓殑绗旇 ID + /** + * ID that smaller than 0 is system folder which is not allowed to + * trash + */ + /* + * 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); // 鑾峰彇 URI 涓殑鏁版嵁 ID + 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); // 閫氱煡绗旇 URI 鍙樺寲 + } + getContext().getContentResolver().notifyChange(uri, null); // 閫氱煡 URI 鍙樺寲 + } + return count; // 杩斿洖鍒犻櫎鐨勮鏁 + } +/* + * 鏇存柊鎿嶄綔銆 + * 璇ユ柟娉曟牴鎹紶鍏ョ殑 URI 鎵ц鐩稿簲鐨勬洿鏂版搷浣滐紝骞惰繑鍥炴洿鏂扮殑琛屾暟銆 + * @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); // 鑾峰彇 URI 涓殑绗旇 ID + 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); // 鑾峰彇 URI 涓殑鏁版嵁 ID + 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); // 閫氱煡绗旇 URI 鍙樺寲 + } + getContext().getContentResolver().notifyChange(uri, null); // 閫氱煡 URI 鍙樺寲 + } + return count; // 杩斿洖鏇存柊鐨勮鏁 + } +/* + * 瑙f瀽閫夋嫨鏉′欢锛岀敤浜庡湪鏌ヨ鎴栨洿鏂版椂闄勫姞棰濆鐨勬潯浠躲 + * @param selection 鍘熷閫夋嫨鏉′欢銆 + * @return 杩斿洖闄勫姞浜嗛澶栨潯浠剁殑瀛楃涓层 + */ + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } +/* + * 澧炲姞绗旇鐗堟湰鍙枫 + *璇ユ柟娉曠敤浜庡鍔犳寚瀹氱瑪璁扮殑鐗堟湰鍙凤紝鎴栬呮牴鎹夋嫨鏉′欢澧炲姞绗﹀悎鏉′欢鐨勭瑪璁扮殑鐗堟湰鍙枫 + * @param id 绗旇 ID锛屽鏋滀负 -1 鍒欒〃绀烘洿鏂版墍鏈夌鍚堟潯浠剁殑绗旇銆 + * @param selection 鏇存柊鏉′欢銆 + * @param selectionArgs 鏇存柊鏉′欢鐨勫弬鏁般 + */ + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); // 鍒涘缓 StringBuilder 瀵硅薄锛岀敤浜庢瀯寤 SQL 璇彞 + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); // 澧炲姞鐗堟湰鍙 + + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); // 鏍规嵁绗旇 ID 鏇存柊 + } + if (!TextUtils.isEmpty(selection)) { + String selectString = id > 0 ? parseSelection(selection) : selection; + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); // 鏇挎崲閫夋嫨鏉′欢涓殑鍗犱綅绗 + } + sql.append(selectString); + } + + mHelper.getWritableDatabase().execSQL(sql.toString()); // 鎵ц SQL 璇彞 + } +/* + * 鑾峰彇 MIME 绫诲瀷銆 + * @param uri 鏌ヨ鐨 URI銆 + * @return 杩斿洖 MIME 绫诲瀷銆 + */ + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +} -- 2.34.1 From 21cf85e072e4143539800a5d1c4fd70b9bd39970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:54:42 +0800 Subject: [PATCH 12/44] Signed-off-by: --- MetaData.txt | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 MetaData.txt diff --git a/MetaData.txt b/MetaData.txt new file mode 100644 index 0000000..eb000f7 --- /dev/null +++ b/MetaData.txt @@ -0,0 +1,115 @@ +/* + * 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.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +/* + *MetaData 绫荤户鎵胯嚜 Task 绫伙紝鐢ㄤ簬澶勭悊涓 Google Task 鐩稿叧鐨勫厓鏁版嵁銆 + * 璇ョ被涓昏鐢ㄤ簬瀛樺偍鍜岃幏鍙栦笌 Google Task 鐩稿叧鐨勫厓鏁版嵁淇℃伅銆 + */ +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); + + private String mRelatedGid = null; // 鐩稿叧 Google Task 鐨 ID +/* + * 璁剧疆鍏冩暟鎹俊鎭 + * 璇ユ柟娉曠敤浜庡皢 Google Task 鐨 ID 鍜屽厓鏁版嵁淇℃伅瀛樺偍鍒颁换鍔$殑澶囨敞瀛楁涓 + * @param gid Google Task 鐨 ID銆 + * @param metaInfo 鍏冩暟鎹俊鎭殑 JSON 瀵硅薄銆 + */ + public void setMeta(String gid, JSONObject metaInfo) { + try { + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); // 灏 Google Task 鐨 ID 娣诲姞鍒板厓鏁版嵁淇℃伅涓 + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); // 璁板綍鏃ュ織锛屾坊鍔犲け璐 + } + setNotes(metaInfo.toString()); // 灏嗗厓鏁版嵁淇℃伅瀛樺偍鍒颁换鍔$殑澶囨敞瀛楁涓 + setName(GTaskStringUtils.META_NOTE_NAME); // 璁剧疆浠诲姟鐨勫悕绉颁负鍏冩暟鎹悕绉 + } +/* + * 鑾峰彇鐩稿叧 Google Task 鐨 ID銆 + * @return 杩斿洖鐩稿叧 Google Task 鐨 ID銆 + */ + public String getRelatedGid() { + return mRelatedGid; + } +/* + * 鍒ゆ柇浠诲姟鏄惁鍊煎緱淇濆瓨銆 + * 璇ユ柟娉曠敤浜庡垽鏂换鍔℃槸鍚﹀寘鍚湁鏁堢殑澶囨敞淇℃伅銆 + * @return 濡傛灉浠诲姟鍖呭惈鏈夋晥鐨勫娉ㄤ俊鎭紝鍒欒繑鍥 true锛屽惁鍒欒繑鍥 false銆 + */ + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } +/* + * 鏍规嵁杩滅▼ JSON 瀵硅薄璁剧疆浠诲姟鍐呭銆 + * 璇ユ柟娉曠敤浜庝粠杩滅▼ JSON 瀵硅薄涓彁鍙栧厓鏁版嵁淇℃伅锛屽苟璁剧疆鐩稿叧 Google Task 鐨 ID銆 + * @param js 杩滅▼ JSON 瀵硅薄銆 + */ + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); // 璋冪敤鐖剁被鏂规硶璁剧疆浠诲姟鍐呭 + if (getNotes() != null) { + try { + JSONObject metaInfo = new JSONObject(getNotes().trim()); // 瑙f瀽澶囨敞瀛楁涓殑鍏冩暟鎹俊鎭 + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); // 鑾峰彇鐩稿叧 Google Task 鐨 ID + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); // 璁板綍鏃ュ織锛岃幏鍙栧け璐 + mRelatedGid = null; + } + } + } +/* + * 鏍规嵁鏈湴 JSON 瀵硅薄璁剧疆浠诲姟鍐呭銆 + * 璇ユ柟娉曚笉搴旇璋冪敤锛屽洜涓哄厓鏁版嵁浠诲姟涓嶅簲浠庢湰鍦 JSON 瀵硅薄涓缃唴瀹广 + * @param js 鏈湴 JSON 瀵硅薄銆 + */ + @Override + public void setContentByLocalJSON(JSONObject js) { + // this function should not be called + // 璇ユ柟娉曚笉搴旇璋冪敤 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } +/* + * 浠庝换鍔″唴瀹逛腑鑾峰彇鏈湴 JSON 瀵硅薄銆 + * 璇ユ柟娉曚笉搴旇璋冪敤锛屽洜涓哄厓鏁版嵁浠诲姟涓嶅簲鐢熸垚鏈湴 JSON 瀵硅薄銆 + * @return 鎶涘嚭寮傚父锛岃〃绀鸿鏂规硶涓嶅簲琚皟鐢ㄣ + */ + @Override + public JSONObject getLocalJSONFromContent() { + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } +/* + * 鑾峰彇鍚屾鎿嶄綔銆 + * 璇ユ柟娉曚笉搴旇璋冪敤锛屽洜涓哄厓鏁版嵁浠诲姟涓嶅簲杩涜鍚屾鎿嶄綔銆 + * @param c 鏁版嵁搴撴父鏍囥 + * @return 鎶涘嚭寮傚父锛岃〃绀鸿鏂规硶涓嶅簲琚皟鐢ㄣ + */ + @Override + public int getSyncAction(Cursor c) { + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } + +} -- 2.34.1 From 3fe057b4382a4535bb6b776eb3ddfe466f39b2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:56:43 +0800 Subject: [PATCH 13/44] Signed-off-by: --- Node.txt | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 Node.txt diff --git a/Node.txt b/Node.txt new file mode 100644 index 0000000..4a2683b --- /dev/null +++ b/Node.txt @@ -0,0 +1,158 @@ +/* + * 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 org.json.JSONObject; +/* + * Node 绫绘槸涓涓娊璞$被锛岀敤浜庤〃绀 Google Task 涓殑鑺傜偣銆 + * 璇ョ被瀹氫箟浜嗚妭鐐圭殑鍩烘湰灞炴у拰鎿嶄綔锛屽苟鎻愪緵浜嗘娊璞℃柟娉曚緵瀛愮被瀹炵幇銆 + */ +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; // Google Task 鐨 ID + + private String mName; // 鑺傜偣鍚嶇О + + private long mLastModified; // 鏈鍚庝慨鏀规椂闂 + + private boolean mDeleted; // 鏄惁宸插垹闄 +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栬妭鐐瑰睘鎬с + */ + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } +/* + * 鑾峰彇鍒涘缓鎿嶄綔鐨 JSON 瀵硅薄銆 + * 璇ユ柟娉曠敤浜庣敓鎴愬垱寤烘搷浣滅殑 JSON 瀵硅薄銆 + * @param actionId 鎿嶄綔 ID銆 + * @return 杩斿洖鍒涘缓鎿嶄綔鐨 JSON 瀵硅薄銆 + */ + public abstract JSONObject getCreateAction(int actionId); +/* + * 鑾峰彇鏇存柊鎿嶄綔鐨 JSON 瀵硅薄銆 + * 璇ユ柟娉曠敤浜庣敓鎴愭洿鏂版搷浣滅殑 JSON 瀵硅薄銆 + * @param actionId 鎿嶄綔 ID銆 + * @return 杩斿洖鏇存柊鎿嶄綔鐨 JSON 瀵硅薄銆 + */ + public abstract JSONObject getUpdateAction(int actionId); +/* + * 鏍规嵁杩滅▼ JSON 瀵硅薄璁剧疆鑺傜偣鍐呭銆 + * 璇ユ柟娉曠敤浜庝粠杩滅▼ JSON 瀵硅薄涓彁鍙栬妭鐐瑰唴瀹广 + * @param js 杩滅▼ JSON 瀵硅薄銆 + */ + public abstract void setContentByRemoteJSON(JSONObject js); +/* + * 鏍规嵁鏈湴 JSON 瀵硅薄璁剧疆鑺傜偣鍐呭銆 + * 璇ユ柟娉曠敤浜庝粠鏈湴 JSON 瀵硅薄涓彁鍙栬妭鐐瑰唴瀹广 + * @param js 鏈湴 JSON 瀵硅薄銆 + */ + public abstract void setContentByLocalJSON(JSONObject js); +/* + *浠庤妭鐐瑰唴瀹逛腑鑾峰彇鏈湴 JSON 瀵硅薄銆 + * 璇ユ柟娉曠敤浜庡皢鑺傜偣鍐呭杞崲涓烘湰鍦 JSON 瀵硅薄銆 + * @return 杩斿洖鏈湴 JSON 瀵硅薄銆 + */ + public abstract JSONObject getLocalJSONFromContent(); +/* + * 鑾峰彇鍚屾鎿嶄綔銆 + * 璇ユ柟娉曠敤浜庢牴鎹暟鎹簱娓告爣鑾峰彇鍚屾鎿嶄綔銆 + * @param c 鏁版嵁搴撴父鏍囥 + * @return 杩斿洖鍚屾鎿嶄綔銆 + */ + public abstract int getSyncAction(Cursor c); +/* + * 璁剧疆 Google Task 鐨 ID銆 + * @param gid Google Task 鐨 ID銆 + */ + public void setGid(String gid) { + this.mGid = gid; + } +/* + * 璁剧疆鑺傜偣鍚嶇О銆 + * @param name 鑺傜偣鍚嶇О銆 + */ + public void setName(String name) { + this.mName = name; + } +/* + * 璁剧疆鏈鍚庝慨鏀规椂闂淬 + * @param lastModified 鏈鍚庝慨鏀规椂闂淬 + */ + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } +/* + * 璁剧疆鑺傜偣鏄惁宸插垹闄ゃ + * @param deleted 鏄惁宸插垹闄ゃ + */ + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } +/* + * 鑾峰彇 Google Task 鐨 ID + * @return 杩斿洖 Google Task 鐨 ID銆 + */ + public String getGid() { + return this.mGid; + } +/* + * 鑾峰彇鑺傜偣鍚嶇О銆 + * @return 杩斿洖鑺傜偣鍚嶇О銆 + */ + public String getName() { + return this.mName; + } +/* + * 鑾峰彇鏈鍚庝慨鏀规椂闂淬 + * @return 杩斿洖鏈鍚庝慨鏀规椂闂淬 + */ + public long getLastModified() { + return this.mLastModified; + } +/* + * 鑾峰彇鑺傜偣鏄惁宸插垹闄ゃ + * @return 杩斿洖鑺傜偣鏄惁宸插垹闄ゃ + */ + public boolean getDeleted() { + return this.mDeleted; + } + +} -- 2.34.1 From 7e214826b082f1ed68f645820e2464ae118a84e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 01:58:39 +0800 Subject: [PATCH 14/44] Signed-off-by: --- SqlData.txt | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 SqlData.txt diff --git a/SqlData.txt b/SqlData.txt new file mode 100644 index 0000000..210fe1b --- /dev/null +++ b/SqlData.txt @@ -0,0 +1,243 @@ +/* + * 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.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; + +/* + * SqlData 绫荤敤浜庡鐞嗕笌鏁版嵁搴撶浉鍏崇殑鏁版嵁鎿嶄綔銆 + * 璇ョ被涓昏鐢ㄤ簬鍔犺浇銆佽缃佽幏鍙栧拰鎻愪氦鏁版嵁銆 + */ +public class SqlData { + private static final String TAG = SqlData.class.getSimpleName(); + + private static final int INVALID_ID = -99999; // 鏃犳晥鐨 ID + // 鏁版嵁琛ㄧ殑鎶曞奖瀛楁 + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + // 鏁版嵁琛ㄧ殑鍒楃储寮 + 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; + + private ContentResolver mContentResolver; // 鍐呭瑙f瀽鍣 + + private boolean mIsCreate; // 鏄惁涓哄垱寤烘搷浣 + + private long mDataId; // 鏁版嵁 ID + + private String mDataMimeType; // 鏁版嵁 MIME 绫诲瀷 + + private String mDataContent; // 鏁版嵁鍐呭 + + private long mDataContentData1; // 鏁版嵁鍐呭鏁版嵁 1 + + private String mDataContentData3; // 鏁版嵁鍐呭鏁版嵁 3 + + private ContentValues mDiffDataValues; // 宸紓鏁版嵁鍊 +/* + * 鏋勯犲嚱鏁帮紝鐢ㄤ簬鍒涘缓鏂扮殑鏁版嵁瀵硅薄銆 + * 璇ユ瀯閫犲嚱鏁板垵濮嬪寲鏁版嵁瀵硅薄鐨勫悇涓睘鎬э紝骞惰缃负鍒涘缓鎿嶄綔銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + */ + public SqlData(Context context) { + mContentResolver = context.getContentResolver(); // 鑾峰彇鍐呭瑙f瀽鍣 + mIsCreate = true; // 璁剧疆涓哄垱寤烘搷浣 + mDataId = INVALID_ID; // 鍒濆鍖栨暟鎹 ID 涓烘棤鏁堝 + mDataMimeType = DataConstants.NOTE; // 鍒濆鍖栨暟鎹 MIME 绫诲瀷涓虹瑪璁扮被鍨 + mDataContent = ""; // 鍒濆鍖栨暟鎹唴瀹逛负绌哄瓧绗︿覆 + mDataContentData1 = 0; // 鍒濆鍖栨暟鎹唴瀹规暟鎹 1 涓 0 + mDataContentData3 = ""; // 鍒濆鍖栨暟鎹唴瀹规暟鎹 3 涓虹┖瀛楃涓 + mDiffDataValues = new ContentValues(); // 鍒濆鍖栧樊寮傛暟鎹 + } +/* + * 鏋勯犲嚱鏁帮紝鐢ㄤ簬浠庢暟鎹簱娓告爣鍔犺浇鏁版嵁瀵硅薄銆 + * 璇ユ瀯閫犲嚱鏁板垵濮嬪寲鏁版嵁瀵硅薄鐨勫悇涓睘鎬э紝骞惰缃负闈炲垱寤烘搷浣溿 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param c 鏁版嵁搴撴父鏍囥 + */ + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); // 鑾峰彇鍐呭瑙f瀽鍣 + mIsCreate = false; // 璁剧疆涓洪潪鍒涘缓鎿嶄綔 + loadFromCursor(c); // 浠庢暟鎹簱娓告爣鍔犺浇鏁版嵁 + mDiffDataValues = new ContentValues(); // 鍒濆鍖栧樊寮傛暟鎹 + } +/* + * 浠庢暟鎹簱娓告爣鍔犺浇鏁版嵁銆 + * 璇ユ柟娉曚粠鏁版嵁搴撴父鏍囦腑鎻愬彇鏁版嵁锛屽苟鍒濆鍖栨暟鎹璞$殑鍚勪釜灞炴с + * @param c 鏁版嵁搴撴父鏍囥 + */ + 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 瀵硅薄涓彁鍙栨暟鎹紝骞舵洿鏂版暟鎹璞$殑鍚勪釜灞炴с + * 濡傛灉鏁版嵁瀵硅薄鏄柊鍒涘缓鐨勶紝鎴栬 JSON 瀵硅薄涓殑鏁版嵁涓庡綋鍓嶆暟鎹璞$殑鏁版嵁涓嶅悓锛屽垯鏇存柊宸紓鏁版嵁鍊笺 + * @param js JSON 瀵硅薄锛屽寘鍚暟鎹唴瀹广 + * @throws JSONException 濡傛灉瑙f瀽 JSON 瀵硅薄鏃跺彂鐢熷紓甯搞 + */ + public void setContent(JSONObject js) throws JSONException { + // 浠 JSON 瀵硅薄涓幏鍙栨暟鎹 ID锛屽鏋 JSON 瀵硅薄涓病鏈夋暟鎹 ID锛屽垯浣跨敤鏃犳晥 ID + long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; + // 濡傛灉鏁版嵁瀵硅薄鏄柊鍒涘缓鐨勶紝鎴栬呮暟鎹 ID 涓嶅悓锛屽垯鏇存柊宸紓鏁版嵁鍊 + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); + } + mDataId = dataId; // 鏇存柊鏁版嵁 ID + // 浠 JSON 瀵硅薄涓幏鍙栨暟鎹 MIME 绫诲瀷锛屽鏋 JSON 瀵硅薄涓病鏈夋暟鎹 MIME 绫诲瀷锛屽垯浣跨敤榛樿鐨勭瑪璁扮被鍨 + 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); + } + mDataMimeType = dataMimeType; // 鏇存柊鏁版嵁 MIME 绫诲瀷 + // 浠 JSON 瀵硅薄涓幏鍙栨暟鎹唴瀹癸紝濡傛灉 JSON 瀵硅薄涓病鏈夋暟鎹唴瀹癸紝鍒欎娇鐢ㄧ┖瀛楃涓 + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + // 濡傛灉鏁版嵁瀵硅薄鏄柊鍒涘缓鐨勶紝鎴栬呮暟鎹唴瀹逛笉鍚岋紝鍒欐洿鏂板樊寮傛暟鎹 + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; // 鏇存柊鏁版嵁鍐呭 + // 浠 JSON 瀵硅薄涓幏鍙栨暟鎹唴瀹规暟鎹 1锛屽鏋 JSON 瀵硅薄涓病鏈夋暟鎹唴瀹规暟鎹 1锛屽垯浣跨敤 0 + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + // 濡傛灉鏁版嵁瀵硅薄鏄柊鍒涘缓鐨勶紝鎴栬呮暟鎹唴瀹规暟鎹 1 涓嶅悓锛屽垯鏇存柊宸紓鏁版嵁鍊 + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; // 鏇存柊鏁版嵁鍐呭鏁版嵁 1 + // 浠 JSON 瀵硅薄涓幏鍙栨暟鎹唴瀹规暟鎹 3锛屽鏋 JSON 瀵硅薄涓病鏈夋暟鎹唴瀹规暟鎹 3锛屽垯浣跨敤绌哄瓧绗︿覆 + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + // 濡傛灉鏁版嵁瀵硅薄鏄柊鍒涘缓鐨勶紝鎴栬呮暟鎹唴瀹规暟鎹 3 涓嶅悓锛屽垯鏇存柊宸紓鏁版嵁鍊 + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; // 鏇存柊鏁版嵁鍐呭鏁版嵁 3 + } +/* + * 鑾峰彇鏁版嵁鍐呭銆 + * 璇ユ柟娉曞皢鏁版嵁瀵硅薄鐨勫悇涓睘鎬ц浆鎹负 JSON 瀵硅薄锛屽苟杩斿洖璇 JSON 瀵硅薄銆 + * @return 杩斿洖鍖呭惈鏁版嵁鍐呭鐨 JSON 瀵硅薄銆 + * @throws JSONException 濡傛灉鐢熸垚 JSON 瀵硅薄鏃跺彂鐢熷紓甯搞 + */ + public JSONObject getContent() throws JSONException { + // 濡傛灉鏁版嵁瀵硅薄鏄柊鍒涘缓鐨勶紝鍒欒褰曢敊璇棩蹇楀苟杩斿洖 null + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + JSONObject js = new JSONObject(); // 鍒涘缓涓涓柊鐨 JSON 瀵硅薄 + js.put(DataColumns.ID, mDataId); // 灏嗘暟鎹 ID 娣诲姞鍒 JSON 瀵硅薄涓 + js.put(DataColumns.MIME_TYPE, mDataMimeType); // 灏嗘暟鎹 MIME 绫诲瀷娣诲姞鍒 JSON 瀵硅薄涓 + js.put(DataColumns.CONTENT, mDataContent); // 灏嗘暟鎹唴瀹规坊鍔犲埌 JSON 瀵硅薄涓 + js.put(DataColumns.DATA1, mDataContentData1); // 灏嗘暟鎹唴瀹规暟鎹 1 娣诲姞鍒 JSON 瀵硅薄涓 + js.put(DataColumns.DATA3, mDataContentData3); // 灏嗘暟鎹唴瀹规暟鎹 3 娣诲姞鍒 JSON 瀵硅薄涓 + return js; // 杩斿洖 JSON 瀵硅薄 + } +/* + * 鎻愪氦鏁版嵁銆 + * 璇ユ柟娉曟牴鎹暟鎹璞$殑鐘舵侊紙鍒涘缓鎴栨洿鏂帮級鎵ц鐩稿簲鐨勬暟鎹簱鎿嶄綔銆 + * 濡傛灉鏄垱寤烘搷浣滐紝鍒欐彃鍏ユ暟鎹埌鏁版嵁搴撲腑锛涘鏋滄槸鏇存柊鎿嶄綔锛屽垯鏇存柊鏁版嵁搴撲腑鐨勬暟鎹 + * @param noteId 绗旇 ID銆 + * @param validateVersion 鏄惁楠岃瘉鐗堟湰銆 + * @param version 鐗堟湰鍙枫 + */ + public void commit(long noteId, boolean validateVersion, long version) { + // 濡傛灉鏄垱寤烘搷浣 + if (mIsCreate) { + // 濡傛灉鏁版嵁 ID 鏄棤鏁堢殑锛屽苟涓斿樊寮傛暟鎹间腑鍖呭惈鏁版嵁 ID锛屽垯绉婚櫎鏁版嵁 ID + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + // 灏嗙瑪璁 ID 娣诲姞鍒板樊寮傛暟鎹间腑 + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + // 鎻掑叆鏁版嵁鍒版暟鎹簱涓 + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); + try { + // 鑾峰彇鎻掑叆鏁版嵁鍚庣殑鏁版嵁 ID + mDataId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + // 璁板綍閿欒鏃ュ織锛屽苟鎶涘嚭鎿嶄綔澶辫触寮傚父 + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + } 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銆 + * 璇ユ柟娉曡繑鍥炴暟鎹璞$殑鏁版嵁 ID銆 + * @return 杩斿洖鏁版嵁 ID銆 + */ + public long getId() { + return mDataId; + } +} \ No newline at end of file -- 2.34.1 From 5aa27e7fbbb7f7af29d1198f7881003cb6794257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:00:35 +0800 Subject: [PATCH 15/44] Signed-off-by: --- SqlNote.txt | 618 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 SqlNote.txt diff --git a/SqlNote.txt b/SqlNote.txt new file mode 100644 index 0000000..2469f48 --- /dev/null +++ b/SqlNote.txt @@ -0,0 +1,618 @@ +/* + * 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(); + + private static final int INVALID_ID = -99999; // 鏃犳晥鐨 ID + // 绗旇琛ㄧ殑鎶曞奖瀛楁 + 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; // 鍐呭瑙f瀽鍣 + + 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; // 鏁版嵁鍒楄〃 +/* + * 鏋勯犲嚱鏁帮紝鐢ㄤ簬鍒涘缓鏂扮殑绗旇瀵硅薄銆 + * 璇ユ瀯閫犲嚱鏁板垵濮嬪寲绗旇瀵硅薄鐨勫悇涓睘鎬э紝骞惰缃负鍒涘缓鎿嶄綔銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + */ + public SqlNote(Context context) { + mContext = context; // 璁剧疆搴旂敤绋嬪簭涓婁笅鏂 + mContentResolver = context.getContentResolver(); // 鑾峰彇鍐呭瑙f瀽鍣 + mIsCreate = true; // 璁剧疆涓哄垱寤烘搷浣 + mId = INVALID_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 涓烘棤鏁堝 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 鍒濆鍖栧皬閮ㄤ欢绫诲瀷涓烘棤鏁堝 + mOriginParent = 0; // 鍒濆鍖栧師濮嬬埗绗旇 ID 涓 0 + mVersion = 0; // 鍒濆鍖栫増鏈彿涓 0 + mDiffNoteValues = new ContentValues(); // 鍒濆鍖栧樊寮傜瑪璁板 + mDataList = new ArrayList(); // 鍒濆鍖栨暟鎹垪琛 + } +/* + * 鏋勯犲嚱鏁帮紝鐢ㄤ簬浠庢暟鎹簱娓告爣鍔犺浇绗旇瀵硅薄銆 + * 璇ユ瀯閫犲嚱鏁板垵濮嬪寲绗旇瀵硅薄鐨勫悇涓睘鎬э紝骞惰缃负闈炲垱寤烘搷浣溿 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param c 鏁版嵁搴撴父鏍囥 + */ + public SqlNote(Context context, Cursor c) { + mContext = context; // 璁剧疆搴旂敤绋嬪簭涓婁笅鏂 + mContentResolver = context.getContentResolver(); // 鑾峰彇鍐呭瑙f瀽鍣 + mIsCreate = false; // 璁剧疆涓洪潪鍒涘缓鎿嶄綔 + loadFromCursor(c); // 浠庢暟鎹簱娓告爣鍔犺浇绗旇瀵硅薄 + mDataList = new ArrayList(); // 鍒濆鍖栨暟鎹垪琛 + if (mType == Notes.TYPE_NOTE) // 濡傛灉绗旇绫诲瀷涓烘櫘閫氱瑪璁 + loadDataContent(); // 鍔犺浇绗旇鐨勬暟鎹唴瀹 + mDiffNoteValues = new ContentValues(); // 鍒濆鍖栧樊寮傜瑪璁板 + } +/* + * 鏋勯犲嚱鏁帮紝鐢ㄤ簬浠庢暟鎹簱涓姞杞芥寚瀹 ID 鐨勭瑪璁板璞° + * 璇ユ瀯閫犲嚱鏁板垵濮嬪寲绗旇瀵硅薄鐨勫悇涓睘鎬э紝骞惰缃负闈炲垱寤烘搷浣溿 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param id 绗旇 ID銆 + */ + public SqlNote(Context context, long id) { + mContext = context; // 璁剧疆搴旂敤绋嬪簭涓婁笅鏂 + mContentResolver = context.getContentResolver(); // 鑾峰彇鍐呭瑙f瀽鍣 + mIsCreate = false; // 璁剧疆涓洪潪鍒涘缓鎿嶄綔 + loadFromCursor(id); // 浠庢暟鎹簱涓姞杞芥寚瀹 ID 鐨勭瑪璁板璞 + mDataList = new ArrayList(); // 鍒濆鍖栨暟鎹垪琛 + if (mType == Notes.TYPE_NOTE) // 濡傛灉绗旇绫诲瀷涓烘櫘閫氱瑪璁 + loadDataContent(); // 鍔犺浇绗旇鐨勬暟鎹唴瀹 + mDiffNoteValues = new ContentValues(); // 鍒濆鍖栧樊寮傜瑪璁板 + + } +/* + * 浠庢暟鎹簱涓姞杞芥寚瀹 ID 鐨勭瑪璁板璞° + * 璇ユ柟娉曚粠鏁版嵁搴撲腑鏌ヨ鎸囧畾 ID 鐨勭瑪璁版暟鎹紝骞惰皟鐢 `loadFromCursor(Cursor c)` 鏂规硶鍔犺浇绗旇瀵硅薄銆 + * @param id 绗旇 ID銆 + */ + 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) { + c.moveToNext(); // 绉诲姩鍒版父鏍囩殑绗竴琛 + loadFromCursor(c); // 鍔犺浇绗旇瀵硅薄 + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); // 璁板綍璀﹀憡鏃ュ織锛屾父鏍囦负绌 + } + } finally { + if (c != null) + c.close(); // 鍏抽棴娓告爣 + } + } +/* + * 浠庢暟鎹簱娓告爣鍔犺浇绗旇瀵硅薄銆 + * 璇ユ柟娉曚粠鏁版嵁搴撴父鏍囦腑鎻愬彇绗旇鏁版嵁锛屽苟鍒濆鍖栫瑪璁板璞$殑鍚勪釜灞炴с + * @param c 鏁版嵁搴撴父鏍囥 + */ + 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); // 鑾峰彇鐗堟湰鍙 + } +/* + * 鍔犺浇绗旇鐨勬暟鎹唴瀹广 + * 璇ユ柟娉曚粠鏁版嵁搴撲腑鏌ヨ涓庣瑪璁扮浉鍏崇殑鏁版嵁鍐呭锛屽苟灏嗗叾鍔犺浇鍒版暟鎹垪琛ㄤ腑銆 + */ + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); // 娓呯┖鏁版嵁鍒楄〃 + try { + // 鏌ヨ涓庣瑪璁扮浉鍏崇殑鏁版嵁鍐呭 + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c != null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); // 璁板綍璀﹀憡鏃ュ織锛岀瑪璁版病鏈夋暟鎹唴瀹 + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); // 浠庢父鏍囦腑鍔犺浇鏁版嵁 + mDataList.add(data); // 灏嗘暟鎹唴瀹规坊鍔犲埌鏁版嵁鍒楄〃涓 + } + } else { + Log.w(TAG, "loadDataContent: cursor = null"); // 璁板綍璀﹀憡鏃ュ織锛屾父鏍囦负绌 + } + } finally { + if (c != null) + c.close(); // 鍏抽棴娓告爣 + } + } +/* + * 璁剧疆鍐呭鐨勬柟娉曪紝鎺ュ彈涓涓狫SONObject浣滀负鍙傛暟锛屽苟鏍规嵁鍏朵腑鐨勬暟鎹洿鏂板綋鍓嶅璞$殑灞炴с + * @param js 鍖呭惈绗旇淇℃伅鐨凧SONObject + * @return 濡傛灉璁剧疆鎴愬姛杩斿洖true锛屽惁鍒欒繑鍥瀎alse + */ + public boolean setContent(JSONObject js) { + try { + // 浠嶫SONObject涓幏鍙栫瑪璁扮殑鍏冩暟鎹 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 妫鏌ョ瑪璁扮殑绫诲瀷 + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 濡傛灉鏄郴缁熸枃浠跺す锛屽垯涓嶈兘璁剧疆鍐呭锛岃褰曡鍛婃棩蹇 + Log.w(TAG, "cannot set system folder"); + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 濡傛灉鏄枃浠跺す绫诲瀷锛屽垯鍙兘鏇存柊鎽樿鍜岀被鍨 + // for folder we can only update the snnipet and type + // 鑾峰彇鎽樿锛屽鏋滄湭鎻愪緵鍒欓粯璁や负绌哄瓧绗︿覆 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + // 濡傛灉鏄柊寤虹瑪璁版垨鎽樿鏈夊彉鍖栵紝鍒欐洿鏂版憳瑕 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + // 鑾峰彇绫诲瀷锛屽鏋滄湭鎻愪緵鍒欓粯璁や负鏅氱瑪璁扮被鍨 + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + // 濡傛灉鏄柊寤虹瑪璁版垨绫诲瀷鏈夊彉鍖栵紝鍒欐洿鏂扮被鍨 + 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); + // 鑾峰彇绗旇ID锛屽鏋滄湭鎻愪緵鍒欓粯璁や负鏃犳晥ID + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + // 濡傛灉鏄柊寤虹瑪璁版垨ID鏈夊彉鍖栵紝鍒欐洿鏂癐D + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); + } + mId = id; + // 鑾峰彇鎻愰啋鏃ユ湡锛屽鏋滄湭鎻愪緵鍒欓粯璁や负0 + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note + .getLong(NoteColumns.ALERTED_DATE) : 0; + // 濡傛灉鏄柊寤虹瑪璁版垨鎻愰啋鏃ユ湡鏈夊彉鍖栵紝鍒欐洿鏂版彁閱掓棩鏈 + if (mIsCreate || mAlertDate != alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); + } + mAlertDate = alertDate; + // 鑾峰彇鑳屾櫙棰滆壊ID锛屽鏋滄湭鎻愪緵鍒欓粯璁や负榛樿鑳屾櫙棰滆壊ID + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note + .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + // 濡傛灉鏄柊寤虹瑪璁版垨鑳屾櫙棰滆壊ID鏈夊彉鍖栵紝鍒欐洿鏂拌儗鏅鑹睮D + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + mBgColorId = bgColorId; + // 鑾峰彇鍒涘缓鏃ユ湡锛屽鏋滄湭鎻愪緵鍒欓粯璁や负褰撳墠鏃堕棿 + long createDate = note.has(NoteColumns.CREATED_DATE) ? note + .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + // 濡傛灉鏄柊寤虹瑪璁版垨鍒涘缓鏃ユ湡鏈夊彉鍖栵紝鍒欐洿鏂板垱寤烘棩鏈 + if (mIsCreate || mCreatedDate != createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); + } + mCreatedDate = createDate; + // 鑾峰彇鏄惁鏈夐檮浠讹紝濡傛灉鏈彁渚涘垯榛樿涓0锛堟棤闄勪欢锛 + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note + .getInt(NoteColumns.HAS_ATTACHMENT) : 0; + // 濡傛灉鏄柊寤虹瑪璁版垨鏄惁鏈夐檮浠舵湁鍙樺寲锛屽垯鏇存柊鏄惁鏈夐檮浠 + if (mIsCreate || mHasAttachment != hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); + } + mHasAttachment = hasAttachment; + // 鑾峰彇淇敼鏃ユ湡锛屽鏋滄湭鎻愪緵鍒欓粯璁や负褰撳墠鏃堕棿 + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + // 濡傛灉鏄柊寤虹瑪璁版垨淇敼鏃ユ湡鏈夊彉鍖栵紝鍒欐洿鏂颁慨鏀规棩鏈 + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } + mModifiedDate = modifiedDate; + // 鑾峰彇鐖禝D锛屽鏋滄湭鎻愪緵鍒欓粯璁や负0 + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; + // 濡傛灉鏄柊寤虹瑪璁版垨鐖禝D鏈夊彉鍖栵紝鍒欐洿鏂扮埗ID + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } + mParentId = parentId; + // 鑾峰彇鎽樿锛屽鏋滄湭鎻愪緵鍒欓粯璁や负绌哄瓧绗︿覆 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + // 濡傛灉鏄柊寤虹瑪璁版垨鎽樿鏈夊彉鍖栵紝鍒欐洿鏂版憳瑕 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + // 鑾峰彇绫诲瀷锛屽鏋滄湭鎻愪緵鍒欓粯璁や负鏅氱瑪璁扮被鍨 + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + // 濡傛灉鏄柊寤虹瑪璁版垨绫诲瀷鏈夊彉鍖栵紝鍒欐洿鏂扮被鍨 + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + // 鑾峰彇灏忛儴浠禝D锛屽鏋滄湭鎻愪緵鍒欓粯璁や负鏃犳晥灏忛儴浠禝D + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + // 濡傛灉鏄柊寤虹瑪璁版垨灏忛儴浠禝D鏈夊彉鍖栵紝鍒欐洿鏂板皬閮ㄤ欢ID + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } + mWidgetId = widgetId; + // 鑾峰彇灏忛儴浠剁被鍨嬶紝濡傛灉鏈彁渚涘垯榛樿涓烘棤鏁堝皬閮ㄤ欢绫诲瀷 + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + // 濡傛灉鏄柊寤虹瑪璁版垨灏忛儴浠剁被鍨嬫湁鍙樺寲锛屽垯鏇存柊灏忛儴浠剁被鍨 + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } + mWidgetType = widgetType; + // 鑾峰彇鍘熷鐖禝D锛屽鏋滄湭鎻愪緵鍒欓粯璁や负0 + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + // 濡傛灉鏄柊寤虹瑪璁版垨鍘熷鐖禝D鏈夊彉鍖栵紝鍒欐洿鏂板師濮嬬埗ID + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } + mOriginParent = originParent; + // 閬嶅巻鏁版嵁鏁扮粍锛屾洿鏂版暟鎹垪琛 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + // 濡傛灉鏁版嵁瀵硅薄鍖呭惈ID锛屽垯鍦ㄧ幇鏈夋暟鎹垪琛ㄤ腑鏌ユ壘鍖归厤鐨凷qlData瀵硅薄 + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + // 濡傛灉鏈壘鍒板尮閰嶇殑SqlData瀵硅薄锛屽垯鍒涘缓涓涓柊鐨凷qlData瀵硅薄骞舵坊鍔犲埌鏁版嵁鍒楄〃涓 + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); + } + // 璁剧疆SqlData瀵硅薄鐨勫唴瀹 + sqlData.setContent(data); + } + } + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } + return true; + } +/* + * 鑾峰彇褰撳墠瀵硅薄鐨勫唴瀹癸紝骞跺皢鍏跺皝瑁呬负涓涓狫SONObject杩斿洖銆 + * @return 鍖呭惈褰撳墠瀵硅薄鍐呭鐨凧SONObject锛屽鏋滃彂鐢熼敊璇垯杩斿洖null + */ + public JSONObject getContent() { + try { + // 鍒涘缓涓涓柊鐨凧SONObject鐢ㄤ簬瀛樺偍鍐呭 + JSONObject js = new JSONObject(); + // 濡傛灉褰撳墠瀵硅薄灏氭湭鍦ㄦ暟鎹簱涓垱寤猴紝璁板綍閿欒鏃ュ織骞惰繑鍥瀗ull + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + // 鍒涘缓涓涓柊鐨凧SONObject鐢ㄤ簬瀛樺偍绗旇淇℃伅 + JSONObject note = new JSONObject(); + // 濡傛灉褰撳墠瀵硅薄鏄櫘閫氱瑪璁扮被鍨 + if (mType == Notes.TYPE_NOTE) { + // 灏嗙瑪璁扮殑鍚勪釜灞炴ф坊鍔犲埌note JSONObject涓 + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); + note.put(NoteColumns.CREATED_DATE, mCreatedDate); + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); + note.put(NoteColumns.PARENT_ID, mParentId); + note.put(NoteColumns.SNIPPET, mSnippet); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.WIDGET_ID, mWidgetId); + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + // 灏唍ote JSONObject娣诲姞鍒癹s JSONObject涓 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + // 鍒涘缓涓涓柊鐨凧SONArray鐢ㄤ簬瀛樺偍鏁版嵁鍒楄〃 + JSONArray dataArray = new JSONArray(); + // 閬嶅巻鏁版嵁鍒楄〃锛屽皢姣忎釜SqlData瀵硅薄鐨勫唴瀹规坊鍔犲埌dataArray涓 + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); + if (data != null) { + dataArray.put(data); + } + } + // 灏哾ataArray娣诲姞鍒癹s JSONObject涓 + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + // 濡傛灉褰撳墠瀵硅薄鏄枃浠跺す鎴栫郴缁熸枃浠跺す绫诲瀷 + // 灏嗙瑪璁扮殑ID銆佺被鍨嬪拰鎽樿娣诲姞鍒皀ote JSONObject涓 + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.SNIPPET, mSnippet); + // 灏唍ote JSONObject娣诲姞鍒癹s JSONObject涓 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + } + // 杩斿洖鍖呭惈鍐呭鐨凧SONObject + return js; + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; + } +/* + * 璁剧疆鐖禝D锛屽苟灏嗗叾娣诲姞鍒板樊寮傚间腑銆 + * @param id 鏂扮殑鐖禝D + */ + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } +/* + * 璁剧疆Google浠诲姟ID锛屽苟灏嗗叾娣诲姞鍒板樊寮傚间腑銆 + * @param gid 鏂扮殑Google浠诲姟ID + */ + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } +/* + * 璁剧疆鍚屾ID锛屽苟灏嗗叾娣诲姞鍒板樊寮傚间腑銆 + * @param syncId 鏂扮殑鍚屾ID + */ + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } +/* + * 閲嶇疆鏈湴淇敼鏍囧織锛屽皢鍏惰缃负0銆 + */ + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } +/* + * 鑾峰彇褰撳墠绗旇鐨処D銆 + * @return 褰撳墠绗旇鐨処D + */ + public long getId() { + return mId; + } +/* + * 鑾峰彇褰撳墠绗旇鐨勭埗ID銆 + * @return 褰撳墠绗旇鐨勭埗ID + */ + public long getParentId() { + return mParentId; + } +/* + * 鑾峰彇褰撳墠绗旇鐨勬憳瑕併 + * @return 褰撳墠绗旇鐨勬憳瑕 + */ + public String getSnippet() { + return mSnippet; + } +/* + * 鍒ゆ柇褰撳墠绗旇鏄惁涓烘櫘閫氱瑪璁扮被鍨嬨 + * @return 濡傛灉褰撳墠绗旇鏄櫘閫氱瑪璁扮被鍨嬶紝杩斿洖true锛涘惁鍒欒繑鍥瀎alse + */ + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } +/* + * 鎻愪氦鏇存敼锛屽皢褰撳墠瀵硅薄鐨勫樊寮傚间繚瀛樺埌鏁版嵁搴撲腑銆 + * @param validateVersion 鏄惁楠岃瘉鐗堟湰鍙 + */ + public void commit(boolean validateVersion) { + // 濡傛灉鏄柊寤虹瑪璁 + if (mIsCreate) { + // 濡傛灉ID鏃犳晥涓斿樊寮傚间腑鍖呭惈ID锛屽垯绉婚櫎ID + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } + // 鎻掑叆绗旇鍒版暟鎹簱锛屽苟鑾峰彇鎻掑叆鍚庣殑URI + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + // 浠嶶RI涓彁鍙栨柊鎻掑叆绗旇鐨処D + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + // 濡傛灉鎻愬彇ID澶辫触锛岃褰曢敊璇棩蹇楀苟鎶涘嚭寮傚父 + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + // 濡傛灉ID涓0锛岃〃绀烘彃鍏ュけ璐ワ紝鎶涘嚭寮傚父 + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + // 濡傛灉褰撳墠绗旇鏄櫘閫氱瑪璁扮被鍨 + if (mType == Notes.TYPE_NOTE) { + // 閬嶅巻鏁版嵁鍒楄〃锛屾彁浜ゆ瘡涓猄qlData瀵硅薄鐨勬洿鏀 + 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) + }); + } else { + // 濡傛灉楠岃瘉鐗堟湰鍙凤紝鏇存柊绗旇鍐呭骞舵鏌ョ増鏈彿 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + // 濡傛灉娌℃湁鏇存柊鍐呭锛岃褰曡鍛婃棩蹇 + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + // 濡傛灉褰撳墠绗旇鏄櫘閫氱瑪璁扮被鍨 + if (mType == Notes.TYPE_NOTE) { + // 閬嶅巻鏁版嵁鍒楄〃锛屾彁浜ゆ瘡涓猄qlData瀵硅薄鐨勬洿鏀 + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); + } + } + } + + // refresh local info + // 鍒锋柊鏈湴淇℃伅 + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + // 娓呯┖宸紓鍊煎苟璁剧疆涓洪潪鏂板缓鐘舵 + mDiffNoteValues.clear(); + mIsCreate = false; + } +} -- 2.34.1 From 8c463e0232f7ac15d5ae88344537c306871d80a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:02:38 +0800 Subject: [PATCH 16/44] Signed-off-by: --- Task.txt | 454 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 Task.txt diff --git a/Task.txt b/Task.txt new file mode 100644 index 0000000..c4a736c --- /dev/null +++ b/Task.txt @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/* + * Task绫昏〃绀轰竴涓换鍔★紝缁ф壙鑷狽ode绫汇 + * 璇ョ被鍖呭惈浜嗕换鍔$殑鍚勭灞炴у拰鎿嶄綔鏂规硶锛屽鍒涘缓銆佹洿鏂般佽缃唴瀹圭瓑銆 + */ +public class Task extends Node { + private static final String TAG = Task.class.getSimpleName(); + + private boolean mCompleted; // 浠诲姟鏄惁宸插畬鎴 + + private String mNotes; // 浠诲姟鐨勫娉 + + private JSONObject mMetaInfo; // 浠诲姟鐨勫厓鏁版嵁淇℃伅 + + private Task mPriorSibling; // 鍓嶄竴涓厔寮熶换鍔 + + private TaskList mParent; // 鐖朵换鍔″垪琛 +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栦换鍔″璞° + * 鍒濆鍖栦换鍔$殑瀹屾垚鐘舵併佸娉ㄣ佸墠涓涓厔寮熶换鍔°佺埗浠诲姟鍒楄〃鍜屽厓鏁版嵁淇℃伅銆 + */ + public Task() { + super(); // 璋冪敤鐖剁被Node鐨勬瀯閫犲嚱鏁 + mCompleted = false; // 鍒濆鍖栦换鍔′负鏈畬鎴愮姸鎬 + mNotes = null; // 鍒濆鍖栧娉ㄤ负绌 + mPriorSibling = null; // 鍒濆鍖栧墠涓涓厔寮熶换鍔′负绌 + mParent = null; // 鍒濆鍖栫埗浠诲姟鍒楄〃涓虹┖ + mMetaInfo = null; // 鍒濆鍖栧厓鏁版嵁淇℃伅涓虹┖ + } +/* + *鐢熸垚鍒涘缓浠诲姟鐨凧SON瀵硅薄銆 + * @param actionId 鎿嶄綔ID + * @return 鍖呭惈鍒涘缓浠诲姟淇℃伅鐨凧SONObject + */ + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 璁剧疆鎿嶄綔绫诲瀷涓哄垱寤 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + // 璁剧疆鎿嶄綔ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + // 璁剧疆浠诲姟鍦ㄧ埗浠诲姟鍒楄〃涓殑绱㈠紩 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // entity_delta + // 璁剧疆浠诲姟鐨勫疄浣撲俊鎭 + 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); + + // parent_id + // 璁剧疆鐖朵换鍔″垪琛ㄧ殑ID + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // dest_parent_type + // 璁剧疆鐩爣鐖剁被鍨 + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // list_id + // 璁剧疆浠诲姟鍒楄〃鐨処D + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // prior_sibling_id + // 璁剧疆鍓嶄竴涓厔寮熶换鍔$殑ID + if (mPriorSibling != null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 鎶涘嚭鑷畾涔夊紓甯革紝琛ㄧず鐢熸垚JSON瀵硅薄澶辫触 + throw new ActionFailureException("fail to generate task-create jsonobject"); + } + + return js; // 杩斿洖鍖呭惈鍒涘缓浠诲姟淇℃伅鐨凧SONObject + } +/* + * 鐢熸垚鏇存柊浠诲姟鐨凧SON瀵硅薄銆 + * @param actionId 鎿嶄綔ID + * @return 鍖呭惈鏇存柊浠诲姟淇℃伅鐨凧SONObject + */ + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 璁剧疆鎿嶄綔绫诲瀷涓烘洿鏂 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + // 璁剧疆鎿嶄綔ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + // 璁剧疆浠诲姟鐨処D + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + // 璁剧疆浠诲姟鐨勫疄浣撲俊鎭 + 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); + + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 鎶涘嚭鑷畾涔夊紓甯革紝琛ㄧず鐢熸垚JSON瀵硅薄澶辫触 + throw new ActionFailureException("fail to generate task-update jsonobject"); + } + + return js; // 杩斿洖鍖呭惈鏇存柊浠诲姟淇℃伅鐨凧SONObject + } +/* + * 鏍规嵁杩滅▼JSON瀵硅薄璁剧疆浠诲姟鐨勫唴瀹广 + * @param js 鍖呭惈浠诲姟淇℃伅鐨勮繙绋婮SON瀵硅薄 + */ + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + // 濡傛灉JSON瀵硅薄涓寘鍚獻D瀛楁锛屽垯璁剧疆浠诲姟鐨処D + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + // 濡傛灉JSON瀵硅薄涓寘鍚渶鍚庝慨鏀规椂闂村瓧娈碉紝鍒欒缃换鍔$殑鏈鍚庝慨鏀规椂闂 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + // 濡傛灉JSON瀵硅薄涓寘鍚悕绉板瓧娈碉紝鍒欒缃换鍔$殑鍚嶇О + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + // notes + // 濡傛灉JSON瀵硅薄涓寘鍚娉ㄥ瓧娈碉紝鍒欒缃换鍔$殑澶囨敞 + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + + // deleted + // 濡傛灉JSON瀵硅薄涓寘鍚垹闄ょ姸鎬佸瓧娈碉紝鍒欒缃换鍔$殑鍒犻櫎鐘舵 + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + + // completed + // 濡傛灉JSON瀵硅薄涓寘鍚畬鎴愮姸鎬佸瓧娈碉紝鍒欒缃换鍔$殑瀹屾垚鐘舵 + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 鎶涘嚭鑷畾涔夊紓甯革紝琛ㄧず浠嶫SON瀵硅薄鑾峰彇浠诲姟鍐呭澶辫触 + throw new ActionFailureException("fail to get task content from jsonobject"); + } + } + } +/* + * 鏍规嵁鏈湴JSON瀵硅薄璁剧疆浠诲姟鐨勫唴瀹广 + * @param js 鍖呭惈浠诲姟淇℃伅鐨勬湰鍦癑SON瀵硅薄 + */ + public void setContentByLocalJSON(JSONObject js) { + // 妫鏌SON瀵硅薄鏄惁涓虹┖鎴栨槸鍚﹀寘鍚繀瑕佺殑瀛楁 + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) + || !js.has(GTaskStringUtils.META_HEAD_DATA)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + // 鑾峰彇绗旇鐨勫厓鏁版嵁 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 鑾峰彇鏁版嵁鏁扮粍 + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 妫鏌ョ瑪璁扮殑绫诲瀷鏄惁涓烘櫘閫氱瑪璁扮被鍨 + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + Log.e(TAG, "invalid type"); + return; + } + // 閬嶅巻鏁版嵁鏁扮粍锛屾煡鎵綧IME绫诲瀷涓篋ataConstants.NOTE鐨勬暟鎹 + 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) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } +/* + * 鏍规嵁浠诲姟鍐呭鐢熸垚鏈湴JSON瀵硅薄銆 + * @return 鍖呭惈浠诲姟鍐呭鐨勬湰鍦癑SON瀵硅薄锛屽鏋滃彂鐢熼敊璇垯杩斿洖null + */ + public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + if (mMetaInfo == null) { + // new task created from web + // 濡傛灉鏄粠缃戦〉鍒涘缓鐨勬柊浠诲姟 + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; + } + // 鍒涘缓涓涓柊鐨凧SON瀵硅薄 + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + data.put(DataColumns.CONTENT, name); + dataArray.put(data); + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + // synced task + // 濡傛灉鏄凡鍚屾鐨勪换鍔 + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 閬嶅巻鏁版嵁鏁扮粍锛屾洿鏂颁换鍔″悕绉 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + data.put(DataColumns.CONTENT, getName()); + break; + } + } + + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; + } + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } +/* + * 璁剧疆浠诲姟鐨勫厓鏁版嵁淇℃伅銆 + * @param metaData 鍖呭惈鍏冩暟鎹俊鎭殑MetaData瀵硅薄 + */ + public void setMetaInfo(MetaData metaData) { + // 妫鏌etaData瀵硅薄鏄惁涓虹┖锛屽苟涓旀槸鍚﹀寘鍚瑪璁颁俊鎭 + if (metaData != null && metaData.getNotes() != null) { + try { + // 灏嗙瑪璁颁俊鎭浆鎹负JSONObject瀵硅薄 + mMetaInfo = new JSONObject(metaData.getNotes()); + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曡鍛婃棩蹇 + Log.w(TAG, e.toString()); + mMetaInfo = null; + // 濡傛灉杞崲澶辫触锛屽皢mMetaInfo璁剧疆涓簄ull + } + } + } +/* + * 鑾峰彇鍚屾鎿嶄綔绫诲瀷銆 + * @param c 鍖呭惈鏈湴绗旇淇℃伅鐨凜ursor瀵硅薄 + * @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瀛楁锛岃〃绀鸿繙绋嬬瑪璁癐D宸茶鍒犻櫎 + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + // validate the note id now + // 楠岃瘉绗旇ID鏄惁鍖归厤 + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + // 濡傛灉鏈湴娌℃湁淇敼 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + // 濡傛灉娌℃湁鏇存柊锛岃繑鍥濻YNC_ACTION_NONE + return SYNC_ACTION_NONE; + } else { + // apply remote to local + // 搴旂敤杩滅▼鏇存柊鍒版湰鍦 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // validate gtask id + // 楠岃瘉Google浠诲姟ID鏄惁鍖归厤 + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + // 濡傛灉鍚屾ID涓庢渶鍚庝慨鏀规椂闂村尮閰嶏紝琛ㄧず鍙湁鏈湴淇敼 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + 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; // 榛樿杩斿洖SYNC_ACTION_ERROR + } +/* + * 鍒ゆ柇浠诲姟鏄惁鍊煎緱淇濆瓨銆 + * @return 濡傛灉浠诲姟鍊煎緱淇濆瓨锛岃繑鍥瀟rue锛涘惁鍒欒繑鍥瀎alse + */ + public boolean isWorthSaving() { + // 妫鏌ュ厓鏁版嵁淇℃伅鏄惁瀛樺湪锛屾垨鑰呬换鍔″悕绉板拰澶囨敞鏄惁涓嶄负绌轰笖涓嶄负绌虹櫧瀛楃涓 + return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) + || (getNotes() != null && getNotes().trim().length() > 0); + } +/* + * 璁剧疆浠诲姟鐨勫畬鎴愮姸鎬併 + * @param completed 浠诲姟鐨勫畬鎴愮姸鎬侊紝true琛ㄧず宸插畬鎴愶紝false琛ㄧず鏈畬鎴 + */ + public void setCompleted(boolean completed) { + this.mCompleted = completed; + } +/* + * 璁剧疆浠诲姟鐨勫娉ㄣ + * @param notes 浠诲姟鐨勫娉 + */ + public void setNotes(String notes) { + this.mNotes = notes; + } +/* + * 璁剧疆鍓嶄竴涓厔寮熶换鍔° + * @param priorSibling 鍓嶄竴涓厔寮熶换鍔 + */ + public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; + } +/* + * 璁剧疆鐖朵换鍔″垪琛ㄣ + * @param parent 鐖朵换鍔″垪琛 + */ + public void setParent(TaskList parent) { + this.mParent = parent; + } +/* + * 鑾峰彇浠诲姟鐨勫畬鎴愮姸鎬併 + * @return 浠诲姟鐨勫畬鎴愮姸鎬侊紝true琛ㄧず宸插畬鎴愶紝false琛ㄧず鏈畬鎴 + */ + public boolean getCompleted() { + return this.mCompleted; + } +/* + * 鑾峰彇浠诲姟鐨勫娉ㄣ + * @return 浠诲姟鐨勫娉 + */ + public String getNotes() { + return this.mNotes; + } +/* + * 鑾峰彇鍓嶄竴涓厔寮熶换鍔° + * @return 鍓嶄竴涓厔寮熶换鍔 + */ + public Task getPriorSibling() { + return this.mPriorSibling; + } +/* + * 鑾峰彇鐖朵换鍔″垪琛ㄣ + * @return 鐖朵换鍔″垪琛 + */ + public TaskList getParent() { + return this.mParent; + } + +} -- 2.34.1 From b21d02340081d5099f89120ef6be74b4355ba81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:04:54 +0800 Subject: [PATCH 17/44] Signed-off-by: --- TaskList.txt | 465 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 TaskList.txt diff --git a/TaskList.txt b/TaskList.txt new file mode 100644 index 0000000..96f681a --- /dev/null +++ b/TaskList.txt @@ -0,0 +1,465 @@ +/* + * 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; + +/* + * TaskList绫昏〃绀轰竴涓换鍔″垪琛紝缁ф壙鑷狽ode绫汇 + * 璇ョ被鍖呭惈浜嗕换鍔″垪琛ㄧ殑鍚勭灞炴у拰鎿嶄綔鏂规硶锛屽鍒涘缓銆佹洿鏂般佽缃唴瀹圭瓑銆 + */ +public class TaskList extends Node { + private static final String TAG = TaskList.class.getSimpleName(); + + private int mIndex; // 浠诲姟鍒楄〃鐨勭储寮 + + private ArrayList mChildren; // 瀛愪换鍔″垪琛 +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栦换鍔″垪琛ㄥ璞° + */ + public TaskList() { + super(); + mChildren = new ArrayList(); + mIndex = 1; + } +/* + * 鐢熸垚鍒涘缓浠诲姟鍒楄〃鐨凧SON瀵硅薄銆 + * @param actionId 鎿嶄綔ID + * @return 鍖呭惈鍒涘缓浠诲姟鍒楄〃淇℃伅鐨凧SONObject + */ + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 璁剧疆鎿嶄綔绫诲瀷涓哄垱寤 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + // 璁剧疆鎿嶄綔ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + // 璁剧疆绱㈠紩 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // entity_delta + // 璁剧疆瀹炰綋淇℃伅 + 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); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); + } + + return js; + } +/* + * 鐢熸垚鏇存柊浠诲姟鍒楄〃鐨凧SON瀵硅薄銆 + * @param actionId 鎿嶄綔ID + * @return 鍖呭惈鏇存柊浠诲姟鍒楄〃淇℃伅鐨凧SONObject + */ + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + // 璁剧疆鎿嶄綔绫诲瀷涓烘洿鏂 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + // 璁剧疆鎿嶄綔ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + // 璁剧疆浠诲姟鍒楄〃鐨処D + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + // 璁剧疆浠诲姟鍒楄〃鐨勫疄浣撲俊鎭 + 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); + + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 鎶涘嚭鑷畾涔夊紓甯革紝琛ㄧず鐢熸垚JSON瀵硅薄澶辫触 + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); + } + + return js; // 杩斿洖鍖呭惈鏇存柊浠诲姟鍒楄〃淇℃伅鐨凧SONObject + } +/* + * 鏍规嵁杩滅▼JSON瀵硅薄璁剧疆浠诲姟鍒楄〃鐨勫唴瀹广 + * @param js 鍖呭惈浠诲姟鍒楄〃淇℃伅鐨勮繙绋婮SON瀵硅薄 + */ + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + // 濡傛灉JSON瀵硅薄涓寘鍚獻D瀛楁锛屽垯璁剧疆浠诲姟鍒楄〃鐨処D + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + // 濡傛灉JSON瀵硅薄涓寘鍚渶鍚庝慨鏀规椂闂村瓧娈碉紝鍒欒缃换鍔″垪琛ㄧ殑鏈鍚庝慨鏀规椂闂 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + // 濡傛灉JSON瀵硅薄涓寘鍚悕绉板瓧娈碉紝鍒欒缃换鍔″垪琛ㄧ殑鍚嶇О + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 鎶涘嚭鑷畾涔夊紓甯革紝琛ㄧず浠嶫SON瀵硅薄鑾峰彇浠诲姟鍒楄〃鍐呭澶辫触 + throw new ActionFailureException("fail to get tasklist content from jsonobject"); + } + } + } +/* + * 鏍规嵁鏈湴JSON瀵硅薄璁剧疆浠诲姟鍒楄〃鐨勫唴瀹广 + * @param js 鍖呭惈浠诲姟鍒楄〃淇℃伅鐨勬湰鍦癑SON瀵硅薄 + */ + public void setContentByLocalJSON(JSONObject js) { + // 妫鏌SON瀵硅薄鏄惁涓虹┖鎴栨槸鍚﹀寘鍚繀瑕佺殑瀛楁 + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + 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) { + // 濡傛灉鏄郴缁熸枃浠跺す绫诲瀷锛屾牴鎹枃浠跺すID璁剧疆浠诲姟鍒楄〃鍚嶇О + 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) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } +/* + * 鏍规嵁浠诲姟鍒楄〃鍐呭鐢熸垚鏈湴JSON瀵硅薄銆 + * @return 鍖呭惈浠诲姟鍒楄〃鍐呭鐨勬湰鍦癑SON瀵硅薄锛屽鏋滃彂鐢熼敊璇垯杩斿洖null + */ + public JSONObject getLocalJSONFromContent() { + try { + // 鍒涘缓涓涓柊鐨凧SON瀵硅薄 + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); + // 鑾峰彇浠诲姟鍒楄〃鐨勫悕绉 + String folderName = getName(); + // 濡傛灉鍚嶇О浠TaskStringUtils.MIUI_FOLDER_PREFFIX寮澶达紝鍒欏幓鎺夊墠缂 + 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); + // 灏嗘枃浠跺す淇℃伅娣诲姞鍒癑SON瀵硅薄涓 + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + // 杩斿洖鍖呭惈浠诲姟鍒楄〃鍐呭鐨勬湰鍦癑SON瀵硅薄 + return js; + } catch (JSONException e) { + // 鎹曡幏JSON寮傚父骞惰褰曢敊璇棩蹇 + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 濡傛灉鍙戠敓閿欒锛岃繑鍥瀗ull + return null; + } + } +/* + * 鑾峰彇鍚屾鎿嶄綔绫诲瀷銆 + * @param c 鍖呭惈鏈湴绗旇淇℃伅鐨凜ursor瀵硅薄 + * @return 鍚屾鎿嶄綔绫诲瀷 + */ + public int getSyncAction(Cursor c) { + try { + // 濡傛灉鏈湴娌℃湁淇敼 + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + // 濡傛灉娌℃湁鏇存柊锛岃繑鍥濻YNC_ACTION_NONE + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + return SYNC_ACTION_NONE; + } else { + // 搴旂敤杩滅▼鏇存柊鍒版湰鍦 + // apply remote to local + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // 楠岃瘉Google浠诲姟ID鏄惁鍖归厤 + // validate gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + // 濡傛灉鍚屾ID涓庢渶鍚庝慨鏀规椂闂村尮閰嶏紝琛ㄧず鍙湁鏈湴淇敼 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // for folder conflicts, just apply local modification + // 瀵逛簬鏂囦欢澶瑰啿绐侊紝鐩存帴搴旂敤鏈湴淇敼 + return SYNC_ACTION_UPDATE_REMOTE; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + // 鎹曡幏寮傚父骞惰褰曢敊璇棩蹇 + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; // 榛樿杩斿洖SYNC_ACTION_ERROR + } +/* + * 鑾峰彇瀛愪换鍔$殑鏁伴噺銆 + * @return 瀛愪换鍔$殑鏁伴噺 + */ + public int getChildTaskCount() { + return mChildren.size(); + } +/* + * 娣诲姞瀛愪换鍔″埌浠诲姟鍒楄〃涓 + * @param task 瑕佹坊鍔犵殑瀛愪换鍔 + * @return 濡傛灉鎴愬姛娣诲姞瀛愪换鍔★紝杩斿洖true锛涘惁鍒欒繑鍥瀎alse + */ + public boolean addChildTask(Task task) { + boolean ret = false; + // 妫鏌ヤ换鍔℃槸鍚︿负绌轰笖鏄惁宸插瓨鍦ㄤ簬瀛愪换鍔″垪琛ㄤ腑 + if (task != null && !mChildren.contains(task)) { + // 灏嗕换鍔℃坊鍔犲埌瀛愪换鍔″垪琛ㄤ腑 + ret = mChildren.add(task); + if (ret) { + // need to set prior sibling and parent + // 闇瑕佽缃墠涓涓厔寮熶换鍔″拰鐖朵换鍔 + task.setPriorSibling(mChildren.isEmpty() ? null : mChildren + .get(mChildren.size() - 1)); + task.setParent(this); + } + } + return ret; + } +/* + * 鍦ㄦ寚瀹氱储寮曚綅缃坊鍔犲瓙浠诲姟鍒颁换鍔″垪琛ㄤ腑銆 + * @param task 瑕佹坊鍔犵殑瀛愪换鍔 + * @param index 瑕佹坊鍔犵殑绱㈠紩浣嶇疆 + * @return 濡傛灉鎴愬姛娣诲姞瀛愪换鍔★紝杩斿洖true锛涘惁鍒欒繑鍥瀎alse + */ + 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); + + // update the task list + // 鏇存柊浠诲姟鍒楄〃 + Task preTask = null; + Task afterTask = null; + if (index != 0) + preTask = mChildren.get(index - 1); + if (index != mChildren.size() - 1) + afterTask = mChildren.get(index + 1); + // 璁剧疆浠诲姟鐨勫墠涓涓厔寮熶换鍔 + task.setPriorSibling(preTask); + // 濡傛灉瀛樺湪鍚庝竴涓厔寮熶换鍔★紝鍒欐洿鏂板叾鍚庝竴涓厔寮熶换鍔$殑鍓嶄竴涓厔寮熶换鍔 + if (afterTask != null) + afterTask.setPriorSibling(task); + } + + return true; + } +/* + * 浠庝换鍔″垪琛ㄤ腑绉婚櫎鎸囧畾鐨勫瓙浠诲姟銆 + * @param task 瑕佺Щ闄ょ殑瀛愪换鍔 + * @return 濡傛灉鎴愬姛绉婚櫎瀛愪换鍔★紝杩斿洖true锛涘惁鍒欒繑鍥瀎alse + */ + public boolean removeChildTask(Task task) { + boolean ret = false; + // 鑾峰彇浠诲姟鍦ㄥ瓙浠诲姟鍒楄〃涓殑绱㈠紩 + int index = mChildren.indexOf(task); + if (index != -1) { + // 浠庡瓙浠诲姟鍒楄〃涓Щ闄や换鍔 + ret = mChildren.remove(task); + + if (ret) { + // reset prior sibling and parent + // 閲嶇疆浠诲姟鐨勫墠涓涓厔寮熶换鍔″拰鐖朵换鍔 + task.setPriorSibling(null); + task.setParent(null); + + // update the task list + // 鏇存柊浠诲姟鍒楄〃 + if (index != mChildren.size()) { + // 濡傛灉绉婚櫎鐨勪换鍔′笉鏄渶鍚庝竴涓换鍔★紝鍒欐洿鏂板叾鍚庝竴涓换鍔$殑鍓嶄竴涓厔寮熶换鍔 + mChildren.get(index).setPriorSibling( + index == 0 ? null : mChildren.get(index - 1)); + } + } + } + return ret; + } +/* + * 灏嗗瓙浠诲姟绉诲姩鍒版寚瀹氱储寮曚綅缃 + * @param task 瑕佺Щ鍔ㄧ殑瀛愪换鍔 + * @param index 鐩爣绱㈠紩浣嶇疆 + * @return 濡傛灉鎴愬姛绉诲姩瀛愪换鍔★紝杩斿洖true锛涘惁鍒欒繑鍥瀎alse + */ + 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 鎵惧埌鐨勫瓙浠诲姟锛屽鏋滄湭鎵惧埌鍒欒繑鍥瀗ull + */ + public Task findChildTaskByGid(String gid) { + // 閬嶅巻瀛愪换鍔″垪琛 + for (int i = 0; i < mChildren.size(); i++) { + Task t = mChildren.get(i); + // 濡傛灉浠诲姟鐨凣ID涓庝紶鍏ョ殑GID鍖归厤锛屽垯杩斿洖璇ヤ换鍔 + if (t.getGid().equals(gid)) { + return t; + } + } + // 濡傛灉鏈壘鍒板尮閰嶇殑浠诲姟锛岃繑鍥瀗ull + return 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 鎵惧埌鐨勫瓙浠诲姟锛屽鏋滄湭鎵惧埌鍒欒繑鍥瀗ull + */ + public Task getChilTaskByGid(String gid) { + for (Task task : mChildren) { + if (task.getGid().equals(gid)) + return task; + } + return null; + } +/* + * 鑾峰彇瀛愪换鍔″垪琛ㄣ + * @return 瀛愪换鍔″垪琛 + */ + public ArrayList getChildTaskList() { + return this.mChildren; + } +/* + * 璁剧疆浠诲姟鍒楄〃鐨勭储寮曘 + * @param index 浠诲姟鍒楄〃鐨勭储寮 + */ + public void setIndex(int index) { + this.mIndex = index; + } +/* + * 鑾峰彇浠诲姟鍒楄〃鐨勭储寮曘 + * @return 浠诲姟鍒楄〃鐨勭储寮 + */ + public int getIndex() { + return this.mIndex; + } +} -- 2.34.1 From d2f3e822a4077a132df8e51046c65bdc593dd7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:08:34 +0800 Subject: [PATCH 18/44] Signed-off-by: --- ActionFailureException.txt | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 ActionFailureException.txt diff --git a/ActionFailureException.txt b/ActionFailureException.txt new file mode 100644 index 0000000..fad6de8 --- /dev/null +++ b/ActionFailureException.txt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * + */ +package net.micode.notes.gtask.exception; +/* + * 鑷畾涔夊紓甯哥被锛岃〃绀烘搷浣滃け璐ャ + * 缁ф壙鑷猂untimeException锛岀敤浜庡湪鎿嶄綔澶辫触鏃舵姏鍑哄紓甯搞 + */ +public class ActionFailureException extends RuntimeException { + private static final long serialVersionUID = 4425249765923293627L; + + public ActionFailureException() { //榛樿鏋勯犲嚱鏁般 + super(); + } +/* + * 甯﹂敊璇俊鎭殑鏋勯犲嚱鏁般 + * @param paramString 閿欒淇℃伅 + */ + public ActionFailureException(String paramString) { + super(paramString); + } +/* + * 甯﹂敊璇俊鎭拰鍘熷洜鐨勬瀯閫犲嚱鏁般 + * @param paramString 閿欒淇℃伅 + * @param paramThrowable 鍘熷洜 + */ + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} -- 2.34.1 From 68a489a68dec959015fb756bd1ee810db6530f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:10:30 +0800 Subject: [PATCH 19/44] Signed-off-by: --- NetworkFailureException.txt | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 NetworkFailureException.txt diff --git a/NetworkFailureException.txt b/NetworkFailureException.txt new file mode 100644 index 0000000..cb7efec --- /dev/null +++ b/NetworkFailureException.txt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; +/* + * 鑷畾涔夊紓甯哥被锛岃〃绀虹綉缁滄搷浣滃け璐ャ + * 缁ф壙鑷狤xception锛岀敤浜庡湪缃戠粶鎿嶄綔澶辫触鏃舵姏鍑哄紓甯搞 + */ +public class NetworkFailureException extends Exception { + private static final long serialVersionUID = 2107610287180234136L; + + public NetworkFailureException() { //榛樿鏋勯犲嚱鏁般 + super(); + } +/* + * 甯﹂敊璇俊鎭殑鏋勯犲嚱鏁般 + * @param paramString 閿欒淇℃伅 + */ + public NetworkFailureException(String paramString) { + super(paramString); + } +/* + * 甯﹂敊璇俊鎭拰鍘熷洜鐨勬瀯閫犲嚱鏁般 + * @param paramString 閿欒淇℃伅 + * @param paramThrowable 鍘熷洜 + */ + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} -- 2.34.1 From cdbaf02f93eb0d2ff53620357a98b231067572dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:12:51 +0800 Subject: [PATCH 20/44] Signed-off-by: --- GTaskASyncTask.txt | 170 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 GTaskASyncTask.txt diff --git a/GTaskASyncTask.txt b/GTaskASyncTask.txt new file mode 100644 index 0000000..97201a8 --- /dev/null +++ b/GTaskASyncTask.txt @@ -0,0 +1,170 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +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绫绘槸涓涓紓姝ヤ换鍔$被锛岀敤浜庢墽琛孏oogle浠诲姟鐨勫悓姝ユ搷浣溿 + * 璇ョ被缁ф壙鑷狝syncTask锛屽苟鍦ㄥ悗鍙扮嚎绋嬩腑鎵ц鍚屾鎿嶄綔锛屽悓鏃跺湪UI绾跨▼涓洿鏂拌繘搴﹀拰缁撴灉銆 + */ +public class GTaskASyncTask extends AsyncTask { + + private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + public interface OnCompleteListener { //瀹氫箟涓涓帴鍙o紝鐢ㄤ簬鍦ㄤ换鍔″畬鎴愭椂閫氱煡鐩戝惉鍣ㄣ + void onComplete(); + } + + private Context mContext; + + private NotificationManager mNotifiManager; + + private GTaskManager mTaskManager; + + private OnCompleteListener mOnCompleteListener; +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栦笂涓嬫枃銆侀氱煡绠$悊鍣ㄣ佷换鍔$鐞嗗櫒鍜屽畬鎴愮洃鍚櫒銆 + * @param context 涓婁笅鏂 + * @param listener 瀹屾垚鐩戝惉鍣 + */ + public GTaskASyncTask(Context context, OnCompleteListener listener) { + // 鍒濆鍖栦笂涓嬫枃 + mContext = context; + // 鍒濆鍖栧畬鎴愮洃鍚櫒 + mOnCompleteListener = listener; + // 鑾峰彇閫氱煡绠$悊鍣 + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + // 鑾峰彇浠诲姟绠$悊鍣ㄥ疄渚 + mTaskManager = GTaskManager.getInstance(); + } +/* + * 鍙栨秷鍚屾鎿嶄綔銆 + */ + public void cancelSync() { + mTaskManager.cancelSync(); + } +/* + * 鍙戝竷杩涘害娑堟伅銆 + */ + public void publishProgess(String message) { + publishProgress(new String[] { + message + }); + } +/* + * 鏄剧ず閫氱煡銆 + * @param tickerId 閫氱煡鐨勬彁绀烘枃鏈琁D + * @param content 閫氱煡鐨勫唴瀹 + */ + private void showNotification(int tickerId, String content) { + // 鍒涘缓涓涓柊鐨凬otification瀵硅薄 + Notification notification = new Notification(R.drawable.notification, mContext + .getString(tickerId), System.currentTimeMillis()); + // 璁剧疆閫氱煡鐨勯粯璁ょ伅鍏夋晥鏋 + notification.defaults = Notification.DEFAULT_LIGHTS; + // 璁剧疆閫氱煡鐨勬爣蹇楋紝鑷姩鍙栨秷閫氱煡 + notification.flags = Notification.FLAG_AUTO_CANCEL; + // 鍒涘缓PendingIntent锛岀敤浜庡湪鐢ㄦ埛鐐瑰嚮閫氱煡鏃跺惎鍔ㄧ浉搴旂殑Activity + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + // 濡傛灉閫氱煡涓嶆槸鎴愬姛閫氱煡锛屽惎鍔∟otesPreferenceActivity + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + + } else { + // 濡傛灉閫氱煡鏄垚鍔熼氱煡锛屽惎鍔∟otesListActivity + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + // 璁剧疆閫氱煡鐨勮缁嗕俊鎭 + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, + pendingIntent); + // 鍙戦侀氱煡 + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } +/* + * 鍦ㄥ悗鍙扮嚎绋嬩腑鎵ц鍚屾鎿嶄綔銆 + * @param unused 鏈娇鐢ㄧ殑鍙傛暟 + * @return 鍚屾缁撴灉鐘舵佺爜 + */ + @Override + protected Integer doInBackground(Void... unused) { + // 鍙戝竷鐧诲綍杩涘害娑堟伅 + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); + // 璋冪敤浠诲姟绠$悊鍣ㄧ殑鍚屾鏂规硶锛屽苟杩斿洖鍚屾缁撴灉鐘舵佺爜 + return mTaskManager.sync(mContext, this); + } +/* + * 鍦║I绾跨▼涓洿鏂拌繘搴︺ + * @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]); + } + } +/* + * 鍦║I绾跨▼涓鐞嗗悓姝ョ粨鏋溿 + * @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)); + } + // 濡傛灉瀛樺湪瀹屾垚鐩戝惉鍣紝鍚姩鏂扮嚎绋嬭皟鐢╫nComplete鏂规硶 + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} -- 2.34.1 From 389902cfa8b4d8129933b46ff518c6bb1db3fc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:15:08 +0800 Subject: [PATCH 21/44] Signed-off-by: --- GTaskClient.txt | 807 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 807 insertions(+) create mode 100644 GTaskClient.txt diff --git a/GTaskClient.txt b/GTaskClient.txt new file mode 100644 index 0000000..1a886a4 --- /dev/null +++ b/GTaskClient.txt @@ -0,0 +1,807 @@ +/* + * 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(); + + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; + + private DefaultHttpClient mHttpClient; + + private String mGetUrl; + + private String mPostUrl; + + private long mClientVersion; + + private boolean mLoggedin; + + private long mLastLoginTime; + + private int mActionId; + + private Account mAccount; + + private JSONArray mUpdateArray; +/* + * 绉佹湁鏋勯犲嚱鏁帮紝鍒濆鍖栨垚鍛樺彉閲忋 + */ + private GTaskClient() { + mHttpClient = null; // 鍒濆鍖朒ttpClient涓簄ull + mGetUrl = GTASK_GET_URL; // 鍒濆鍖朑ET鍜孭OST璇锋眰鐨刄RL + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; // 鍒濆鍖栧鎴风鐗堟湰鍙蜂负-1 + mLoggedin = false; // 鍒濆鍖栫櫥褰曠姸鎬佷负false + mLastLoginTime = 0; // 鍒濆鍖栨渶鍚庝竴娆$櫥褰曟椂闂翠负0 + mActionId = 1; // 鍒濆鍖栨搷浣淚D涓1 + mAccount = null; // 鍒濆鍖栬处鎴蜂负null + mUpdateArray = null; // 鍒濆鍖栨洿鏂版暟缁勪负null + } +/* + * 鑾峰彇GTaskClient鐨勫崟渚嬪疄渚嬨 + * @return GTaskClient鐨勫崟渚嬪疄渚 + */ + public static synchronized GTaskClient getInstance() { + // 濡傛灉mInstance涓簄ull锛屽垱寤轰竴涓柊鐨凣TaskClient瀹炰緥 + if (mInstance == null) { + mInstance = new GTaskClient(); + } + // 杩斿洖GTaskClient鐨勫崟渚嬪疄渚 + return mInstance; + } +/* + * 鐧诲綍Google浠诲姟鏈嶅姟銆 + * @param activity 褰撳墠娲诲姩 + * @return 鏄惁鐧诲綍鎴愬姛 + */ + public boolean login(Activity activity) { + // we suppose that the cookie would expire after 5 minutes + // 鍋囪cookie鍦5鍒嗛挓鍚庤繃鏈燂紝闇瑕侀噸鏂扮櫥褰 + // then we need to re-login + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // need to re-login after account switch + // 闇瑕侀噸鏂扮櫥褰曚互鍒囨崲璐︽埛 + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + // 濡傛灉宸茬粡鐧诲綍锛岀洿鎺ヨ繑鍥瀟rue + 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; + } + + // login with custom domain if necessary + // 濡傛灉璐︽埛涓嶆槸gmail.com鎴杇ooglemail.com锛屽皾璇曚娇鐢ㄨ嚜瀹氫箟鍩熷悕鐧诲綍 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // try to login with google official url + // 灏濊瘯浣跨敤Google瀹樻柟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; + // 鑾峰彇AccountManager瀹炰緥 + AccountManager accountManager = AccountManager.get(activity); + // 鑾峰彇鎵鏈塆oogle璐︽埛 + Account[] accounts = accountManager.getAccountsByType("com.google"); + // 濡傛灉娌℃湁鍙敤鐨凣oogle璐︽埛锛岃褰曢敊璇棩蹇楀苟杩斿洖null + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + // 鑾峰彇璁剧疆涓殑鍚屾璐︽埛鍚嶇О + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + // 閬嶅巻鎵鏈塆oogle璐︽埛锛屾煡鎵句笌璁剧疆涓悓姝ヨ处鎴峰悕绉板尮閰嶇殑璐︽埛 + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + // 濡傛灉鎵惧埌鍖归厤鐨勮处鎴凤紝灏嗗叾璧嬪肩粰mAccount + if (account != null) { + mAccount = account; + } else { + // 濡傛灉娌℃湁鎵惧埌鍖归厤鐨勮处鎴凤紝璁板綍閿欒鏃ュ織骞惰繑鍥瀗ull + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // get the token now + // 鑾峰彇鎺堟潈浠ょ墝 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + // 鑾峰彇鎺堟潈浠ょ墝鐨凚undle + Bundle authTokenBundle = accountManagerFuture.getResult(); + // 浠嶣undle涓幏鍙栨巿鏉冧护鐗 + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 濡傛灉闇瑕佷娇鎺堟潈浠ょ墝澶辨晥锛屼娇浠ょ墝澶辨晥骞堕噸鏂拌幏鍙 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + // 濡傛灉鑾峰彇鎺堟潈浠ょ墝澶辫触锛岃褰曢敊璇棩蹇楀苟杩斿洖null + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; // 杩斿洖鎺堟潈浠ょ墝 + } +/* + * 灏濊瘯鐧诲綍Google浠诲姟鏈嶅姟銆 + * @param activity 褰撳墠娲诲姩 + * @param authToken 鎺堟潈浠ょ墝 + * @return 鏄惁鐧诲綍鎴愬姛 + */ + private boolean tryToLoginGtask(Activity activity, String authToken) { + // 灏濊瘯浣跨敤褰撳墠鎺堟潈浠ょ墝鐧诲綍Google浠诲姟鏈嶅姟 + if (!loginGtask(authToken)) { + // maybe the auth token is out of date, now let's invalidate the + // token and try again + // 濡傛灉鐧诲綍澶辫触锛屽彲鑳芥槸鎺堟潈浠ょ墝宸茶繃鏈燂紝浣夸护鐗屽け鏁堝苟閲嶆柊鑾峰彇 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + // 浣跨敤鏂扮殑鎺堟潈浠ょ墝鍐嶆灏濊瘯鐧诲綍Google浠诲姟鏈嶅姟 + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } +/* + * 浣跨敤鎺堟潈浠ょ墝鐧诲綍Google浠诲姟鏈嶅姟銆 + * @param authToken 鎺堟潈浠ょ墝 + * @return 鏄惁鐧诲綍鎴愬姛 + */ + private boolean loginGtask(String authToken) { + // 璁剧疆杩炴帴瓒呮椂鍜屽鎺ュ瓧瓒呮椂 + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + // 鍒涘缓DefaultHttpClient瀹炰緥 + mHttpClient = new DefaultHttpClient(httpParameters); + // 鍒涘缓BasicCookieStore瀹炰緥骞惰缃埌HttpClient涓 + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + // 璁剧疆HttpClient涓嶄娇鐢‥xpect-Continue + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // login gtask + // 鐧诲綍Google浠诲姟鏈嶅姟 + try { + // 鏋勫缓鐧诲綍URL + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the cookie now + // 鑾峰彇cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // get the client version + // 鑾峰彇瀹㈡埛绔増鏈 + 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) { + // simply catch all exceptions + // 鎹曡幏鎵鏈夊紓甯 + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } +/* + * 鑾峰彇鎿嶄綔ID骞堕掑銆 + * @return 褰撳墠鎿嶄綔ID + */ + private int getActionId() { + // 杩斿洖褰撳墠鎿嶄綔ID骞堕掑 + return mActionId++; + } +/* + * 鍒涘缓HttpPost璇锋眰銆 + * @return 鍒涘缓鐨凥ttpPost璇锋眰 + */ + private HttpPost createHttpPost() { + // 鍒涘缓HttpPost璇锋眰锛岃缃姹俇RL + HttpPost httpPost = new HttpPost(mPostUrl); + // 璁剧疆璇锋眰澶达紝鎸囧畾Content-Type涓篴pplication/x-www-form-urlencoded锛屽瓧绗﹂泦涓篣TF-8 + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + // 璁剧疆璇锋眰澶达紝鎸囧畾AT涓1 + return httpPost; + // 杩斿洖鍒涘缓鐨凥ttpPost璇锋眰 + } +/* + * 浠嶩ttpEntity涓幏鍙栧搷搴斿唴瀹瑰苟杩斿洖瀛楃涓插舰寮忋 + * 鏀寔澶勭悊gzip鍜宒eflate鍘嬬缉缂栫爜銆 + * @param entity HTTP鍝嶅簲瀹炰綋 + * @return 鍝嶅簲鍐呭鐨勫瓧绗︿覆褰㈠紡 + * @throws IOException 濡傛灉璇诲彇杈撳叆娴佹椂鍙戠敓閿欒 + */ + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + // 妫鏌ttpEntity鏄惁鍖呭惈Content-Encoding澶撮儴 + if (entity.getContentEncoding() != null) { + // 鑾峰彇Content-Encoding鐨勫 + contentEncoding = entity.getContentEncoding().getValue(); + // 璁板綍鏃ュ織锛屾樉绀哄綋鍓嶇殑Content-Encoding + Log.d(TAG, "encoding: " + contentEncoding); + } + // 鑾峰彇HttpEntity鐨勫師濮嬭緭鍏ユ祦 + InputStream input = entity.getContent(); + // 濡傛灉Content-Encoding鏄痝zip锛屽垯浣跨敤GZIPInputStream瑙e帇缂 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + // 濡傛灉Content-Encoding鏄痙eflate锛屽垯浣跨敤InflaterInputStream瑙e帇缂 + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + // 鍒涘缓InputStreamReader锛岀敤浜庡皢瀛楄妭娴佽浆鎹负瀛楃娴 + InputStreamReader isr = new InputStreamReader(input); + // 鍒涘缓BufferedReader锛岀敤浜庨愯璇诲彇瀛楃娴 + BufferedReader br = new BufferedReader(isr); + // 鍒涘缓StringBuilder锛岀敤浜庡瓨鍌ㄨ鍙栧埌鐨勫唴瀹 + StringBuilder sb = new StringBuilder(); + // 寰幆璇诲彇杈撳叆娴佷腑鐨勬瘡涓琛 + while (true) { + String buff = br.readLine(); + // 濡傛灉璇诲彇鍒皀ull锛岃〃绀哄凡缁忓埌杈炬祦鐨勬湯灏撅紝杩斿洖StringBuilder涓殑鍐呭 + if (buff == null) { + return sb.toString(); + } + // 灏嗚鍙栧埌鐨勮杩藉姞鍒癝tringBuilder涓 + sb = sb.append(buff); + } + } finally { + // 纭繚杈撳叆娴佸湪鏂规硶缁撴潫鏃惰鍏抽棴 + input.close(); + } + } +/* + * 鍙戦丳OST璇锋眰骞惰繑鍥炲搷搴旂殑JSONObject銆 + * @param js 瑕佸彂閫佺殑璇锋眰鏁版嵁锛屼互JSONObject褰㈠紡琛ㄧず + * @return 鏈嶅姟鍣ㄥ搷搴旂殑JSONObject + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡傛湭鐧诲綍鎴栧搷搴斿唴瀹规棤娉曡浆鎹负JSONObject锛 + */ + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + // 妫鏌ョ敤鎴锋槸鍚﹀凡鐧诲綍锛屽鏋滄湭鐧诲綍鍒欐姏鍑哄紓甯 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + // 鍒涘缓HttpPost瀵硅薄 + HttpPost httpPost = createHttpPost(); + try { + // 鍒涘缓涓涓狶inkedList鏉ュ瓨鍌ㄨ姹傚弬鏁 + LinkedList list = new LinkedList(); + // 灏嗚姹傛暟鎹紙JSONObject锛夎浆鎹负瀛楃涓诧紝骞舵坊鍔犲埌璇锋眰鍙傛暟鍒楄〃涓 + list.add(new BasicNameValuePair("r", js.toString())); + // 鍒涘缓UrlEncodedFormEntity瀵硅薄锛岀敤浜庡皢璇锋眰鍙傛暟缂栫爜涓鸿〃鍗曟牸寮 + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + // 灏嗚姹傚疄浣撹缃埌HttpPost瀵硅薄涓 + httpPost.setEntity(entity); + // execute the post + // 鎵цPOST璇锋眰 + HttpResponse response = mHttpClient.execute(httpPost); + // 鑾峰彇鍝嶅簲鍐呭骞惰浆鎹负瀛楃涓 + String jsString = getResponseContent(response.getEntity()); + // 灏嗗搷搴斿唴瀹瑰瓧绗︿覆杞崲涓篔SONObject骞惰繑鍥 + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + // 鎹曡幏骞惰褰旽TTP鍗忚寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + // 鎹曡幏骞惰褰旾O寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + 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 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡侸SON澶勭悊澶辫触锛 + */ + public void createTask(Task task) throws NetworkFailureException { + // 鎻愪氦鏇存柊鎿嶄綔 + commitUpdate(); + try { + // 鍒涘缓涓涓柊鐨凧SONObject锛岀敤浜庡瓨鍌≒OST璇锋眰鐨勬暟鎹 + JSONObject jsPost = new JSONObject(); + // 鍒涘缓涓涓狫SONArray锛岀敤浜庡瓨鍌ㄤ换鍔$殑鍒涘缓鎿嶄綔 + JSONArray actionList = new JSONArray(); + + // action_list + // 灏嗕换鍔$殑鍒涘缓鎿嶄綔娣诲姞鍒癮ctionList涓 + actionList.put(task.getCreateAction(getActionId())); + // 灏哸ctionList娣诲姞鍒癹sPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 娣诲姞瀹㈡埛绔増鏈俊鎭埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + // 鍙戦丳OST璇锋眰锛屽苟灏嗗搷搴斿唴瀹硅浆鎹负JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 浠庡搷搴斾腑鑾峰彇缁撴灉鏁扮粍锛屽苟鑾峰彇绗竴涓粨鏋 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 浠庣粨鏋滀腑鑾峰彇鏂板垱寤轰换鍔$殑ID锛屽苟璁剧疆鍒颁换鍔″璞′腑 + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) {銆 + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handing jsonobject failed"); + } + } +/* + * 鍒涘缓涓涓柊鐨勪换鍔″垪琛紝骞跺皢鍏朵笂浼犲埌鏈嶅姟鍣ㄣ + * @param tasklist 瑕佸垱寤虹殑浠诲姟鍒楄〃瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡侸SON澶勭悊澶辫触锛 + */ + public void createTaskList(TaskList tasklist) throws NetworkFailureException { + // 鎻愪氦鏇存柊鎿嶄綔 + commitUpdate(); + try { + // 鍒涘缓涓涓柊鐨凧SONObject锛岀敤浜庡瓨鍌≒OST璇锋眰鐨勬暟鎹 + JSONObject jsPost = new JSONObject(); + // 鍒涘缓涓涓狫SONArray锛岀敤浜庡瓨鍌ㄤ换鍔″垪琛ㄧ殑鍒涘缓鎿嶄綔 + JSONArray actionList = new JSONArray(); + + // action_list + // 灏嗕换鍔″垪琛ㄧ殑鍒涘缓鎿嶄綔娣诲姞鍒癮ctionList涓 + actionList.put(tasklist.getCreateAction(getActionId())); + // 灏哸ctionList娣诲姞鍒癹sPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client version + // 娣诲姞瀹㈡埛绔増鏈俊鎭埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + // 鍙戦丳OST璇锋眰锛屽苟灏嗗搷搴斿唴瀹硅浆鎹负JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 浠庡搷搴斾腑鑾峰彇缁撴灉鏁扮粍锛屽苟鑾峰彇绗竴涓粨鏋 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 浠庣粨鏋滀腑鑾峰彇鏂板垱寤轰换鍔″垪琛ㄧ殑ID锛屽苟璁剧疆鍒颁换鍔″垪琛ㄥ璞′腑 + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handing jsonobject failed"); + } + } +/* + * 鎻愪氦鎵鏈夋湭瀹屾垚鐨勬洿鏂版搷浣滃埌鏈嶅姟鍣ㄣ + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡侸SON澶勭悊澶辫触锛 + */ + public void commitUpdate() throws NetworkFailureException { + // 妫鏌ユ槸鍚︽湁鏈畬鎴愮殑鏇存柊鎿嶄綔 + if (mUpdateArray != null) { + try { + // 鍒涘缓涓涓柊鐨凧SONObject锛岀敤浜庡瓨鍌≒OST璇锋眰鐨勬暟鎹 + JSONObject jsPost = new JSONObject(); + + // action_list + // 灏嗘湭瀹屾垚鐨勬洿鏂版搷浣滄暟缁勬坊鍔犲埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + + // client_version + // 娣诲姞瀹㈡埛绔増鏈俊鎭埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 鍙戦丳OST璇锋眰 + postRequest(jsPost); + // 鎻愪氦鎴愬姛鍚庯紝娓呯┖鏇存柊鎿嶄綔鏁扮粍 + mUpdateArray = null; + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("commit update: handing jsonobject failed"); + } + } + } +/* + * 灏嗕竴涓妭鐐规坊鍔犲埌鏇存柊鎿嶄綔鏁扮粍涓紝骞跺湪蹇呰鏃舵彁浜ゆ洿鏂版搷浣溿 + * @param node 瑕佹坊鍔犵殑鑺傜偣瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + */ + public void addUpdateNode(Node node) throws NetworkFailureException { + // 妫鏌ヨ妭鐐规槸鍚︿负绌 + if (node != null) { + // too many update items may result in an error + // set max to 10 items + // 濡傛灉鏇存柊鎿嶄綔鏁扮粍涓凡缁忔湁瓒呰繃10涓」鐩紝鍒欐彁浜ゆ洿鏂版搷浣 + // 閬垮厤杩囧鐨勬洿鏂版搷浣滃鑷撮敊璇 + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate(); + } + // 濡傛灉鏇存柊鎿嶄綔鏁扮粍涓虹┖锛屽垯鍒涘缓涓涓柊鐨凧SONArray + if (mUpdateArray == null) + mUpdateArray = new JSONArray(); + // 灏嗚妭鐐圭殑鏇存柊鎿嶄綔娣诲姞鍒版洿鏂版搷浣滄暟缁勪腑 + mUpdateArray.put(node.getUpdateAction(getActionId())); + } + } +/* + * 灏嗕换鍔′粠涓涓换鍔″垪琛ㄧЩ鍔ㄥ埌鍙︿竴涓换鍔″垪琛ㄣ + * @param task 瑕佺Щ鍔ㄧ殑浠诲姟瀵硅薄 + * @param preParent 浠诲姟鐨勫師濮嬬埗浠诲姟鍒楄〃 + * @param curParent 浠诲姟鐨勬柊鐖朵换鍔″垪琛 + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡侸SON澶勭悊澶辫触锛 + */ + public void moveTask(Task task, TaskList preParent, TaskList curParent) + throws NetworkFailureException { + // 鎻愪氦鏇存柊鎿嶄綔 + commitUpdate(); + try { + // 鍒涘缓涓涓柊鐨凧SONObject锛岀敤浜庡瓨鍌≒OST璇锋眰鐨勬暟鎹 + JSONObject jsPost = new JSONObject(); + // 鍒涘缓涓涓狫SONArray锛岀敤浜庡瓨鍌ㄤ换鍔$殑绉诲姩鎿嶄綔 + JSONArray actionList = new JSONArray(); + // 鍒涘缓涓涓狫SONObject锛岀敤浜庡瓨鍌ㄥ叿浣撶殑绉诲姩鎿嶄綔 + JSONObject action = new JSONObject(); + + // action_list + // 璁剧疆绉诲姩鎿嶄綔鐨勭被鍨嬩负"move" + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + // 璁剧疆绉诲姩鎿嶄綔鐨処D + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 璁剧疆瑕佺Щ鍔ㄧ殑浠诲姟鐨処D + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); + // 濡傛灉浠诲姟鍦ㄥ悓涓涓换鍔″垪琛ㄤ腑绉诲姩锛屽苟涓斾笉鏄涓涓换鍔★紝鍒欒缃墠涓涓厔寮熶换鍔$殑ID + if (preParent == curParent && task.getPriorSibling() != null) { + // put prioring_sibing_id only if moving within the tasklist and + // it is not the first one + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } + // 璁剧疆浠诲姟鐨勫師濮嬬埗浠诲姟鍒楄〃鐨処D + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + // 璁剧疆浠诲姟鐨勬柊鐖朵换鍔″垪琛ㄧ殑ID + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); + // 濡傛灉浠诲姟鍦ㄤ笉鍚岀殑浠诲姟鍒楄〃涔嬮棿绉诲姩锛屽垯璁剧疆鐩爣浠诲姟鍒楄〃鐨処D + if (preParent != curParent) { + // put the dest_list only if moving between tasklists + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + // 灏嗙Щ鍔ㄦ搷浣滄坊鍔犲埌actionList涓 + actionList.put(action); + // 灏哸ctionList娣诲姞鍒癹sPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 娣诲姞瀹㈡埛绔増鏈俊鎭埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 鍙戦丳OST璇锋眰 + postRequest(jsPost); + + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("move task: handing jsonobject failed"); + } + } +/* + * 鍒犻櫎涓涓妭鐐癸紝骞跺皢鍏舵爣璁颁负宸插垹闄ょ姸鎬併 + * @param node 瑕佸垹闄ょ殑鑺傜偣瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡侸SON澶勭悊澶辫触锛 + */ + public void deleteNode(Node node) throws NetworkFailureException { + // 鎻愪氦鏇存柊鎿嶄綔 + commitUpdate(); + try { + // 鍒涘缓涓涓柊鐨凧SONObject锛岀敤浜庡瓨鍌≒OST璇锋眰鐨勬暟鎹 + JSONObject jsPost = new JSONObject(); + // 鍒涘缓涓涓狫SONArray锛岀敤浜庡瓨鍌ㄨ妭鐐圭殑鍒犻櫎鎿嶄綔 + JSONArray actionList = new JSONArray(); + + // action_list + // 灏嗚妭鐐规爣璁颁负宸插垹闄 + node.setDeleted(true); + // 鑾峰彇鑺傜偣鐨勬洿鏂版搷浣滐紝骞跺皢鍏舵坊鍔犲埌actionList涓 + actionList.put(node.getUpdateAction(getActionId())); + // 灏哸ctionList娣诲姞鍒癹sPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 娣诲姞瀹㈡埛绔増鏈俊鎭埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 鍙戦丳OST璇锋眰 + postRequest(jsPost); + // 鎻愪氦鎴愬姛鍚庯紝娓呯┖鏇存柊鎿嶄綔鏁扮粍 + mUpdateArray = null; + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("delete node: handing jsonobject failed"); + } + } +/* + * 鑾峰彇鎵鏈変换鍔″垪琛紝骞惰繑鍥炰竴涓寘鍚换鍔″垪琛ㄧ殑JSONArray銆 + * @return 鍖呭惈浠诲姟鍒楄〃鐨凧SONArray + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡傛湭鐧诲綍鎴朖SON澶勭悊澶辫触锛 + */ + public JSONArray getTaskLists() throws NetworkFailureException { + // 妫鏌ョ敤鎴锋槸鍚﹀凡鐧诲綍锛屽鏋滄湭鐧诲綍鍒欐姏鍑哄紓甯 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + try { + // 鍒涘缓涓涓狧ttpGet瀵硅薄锛岀敤浜庡彂閫丟ET璇锋眰 + HttpGet httpGet = new HttpGet(mGetUrl); + HttpResponse response = null; + // 鎵цGET璇锋眰锛屽苟鑾峰彇鍝嶅簲 + response = mHttpClient.execute(httpGet); + + // get the task list + // 鑾峰彇鍝嶅簲鍐呭骞惰浆鎹负瀛楃涓 + String resString = getResponseContent(response.getEntity()); + // 浠庡搷搴斿唴瀹逛腑鎻愬彇鍖呭惈浠诲姟鍒楄〃鐨凧SON瀛楃涓 + 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 + JSONObject js = new JSONObject(jsString); + // 浠嶫SONObject涓幏鍙栦换鍔″垪琛ㄧ殑JSONArray骞惰繑鍥 + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); + } catch (ClientProtocolException e) { + // 鎹曡幏骞惰褰旽TTP鍗忚寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (IOException e) { + // 鎹曡幏骞惰褰旾O寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task lists: handing jasonobject failed"); + } + } +/* + * 鑾峰彇鎸囧畾浠诲姟鍒楄〃涓殑鎵鏈変换鍔★紝骞惰繑鍥炰竴涓寘鍚换鍔$殑JSONArray銆 + * @param listGid 浠诲姟鍒楄〃鐨処D + * @return 鍖呭惈浠诲姟鐨凧SONArray + * @throws NetworkFailureException 濡傛灉缃戠粶璇锋眰澶辫触 + * @throws ActionFailureException 濡傛灉鎿嶄綔澶辫触锛堜緥濡侸SON澶勭悊澶辫触锛 + */ + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + // 鎻愪氦鏇存柊鎿嶄綔 + commitUpdate(); + try { + // 鍒涘缓涓涓柊鐨凧SONObject锛岀敤浜庡瓨鍌≒OST璇锋眰鐨勬暟鎹 + JSONObject jsPost = new JSONObject(); + // 鍒涘缓涓涓狫SONArray锛岀敤浜庡瓨鍌ㄨ幏鍙栦换鍔″垪琛ㄧ殑鎿嶄綔 + JSONArray actionList = new JSONArray(); + // 鍒涘缓涓涓狫SONObject锛岀敤浜庡瓨鍌ㄥ叿浣撶殑鑾峰彇鎿嶄綔 + JSONObject action = new JSONObject(); + + // action_list + // 璁剧疆鑾峰彇鎿嶄綔鐨勭被鍨嬩负"getall" + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + // 璁剧疆鑾峰彇鎿嶄綔鐨処D + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 璁剧疆瑕佽幏鍙栫殑浠诲姟鍒楄〃鐨処D + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + // 璁剧疆鏄惁鑾峰彇宸插垹闄ょ殑浠诲姟锛岃繖閲岃缃负false + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + // 灏嗚幏鍙栨搷浣滄坊鍔犲埌actionList涓 + actionList.put(action); + // 灏哸ctionList娣诲姞鍒癹sPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 娣诲姞瀹㈡埛绔増鏈俊鎭埌jsPost涓 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 鍙戦丳OST璇锋眰锛屽苟灏嗗搷搴斿唴瀹硅浆鎹负JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 浠庡搷搴斾腑鑾峰彇浠诲姟鍒楄〃鐨凧SONArray骞惰繑鍥 + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); + } catch (JSONException e) { + // 鎹曡幏骞惰褰旿SON瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task list: handing jsonobject failed"); + } + } +/* + * 鑾峰彇褰撳墠鍚屾鐨勮处鎴枫 + * @return 褰撳墠鍚屾鐨勮处鎴峰璞 + */ + public Account getSyncAccount() { + return mAccount; + } +/* + * 閲嶇疆鏇存柊鎿嶄綔鏁扮粍銆 + */ + public void resetUpdateArray() { + mUpdateArray = null; + } +} -- 2.34.1 From be4d9275c05ee51990c31fe0d8b50692d1a9c282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:18:12 +0800 Subject: [PATCH 22/44] Signed-off-by: --- GTaskManager.txt | 1065 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1065 insertions(+) create mode 100644 GTaskManager.txt diff --git a/GTaskManager.txt b/GTaskManager.txt new file mode 100644 index 0000000..45caf8d --- /dev/null +++ b/GTaskManager.txt @@ -0,0 +1,1065 @@ +/* + * 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. + */ +/* + * GTaskManager 璐熻矗绠$悊鏈湴绗旇鏁版嵁搴撲笌 Google Tasks 涔嬮棿鐨勫悓姝ヨ繃绋嬨 + */ +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; + +/* + * GTaskManager 璐熻矗绠$悊鏈湴绗旇鏁版嵁搴撲笌 Google Tasks 涔嬮棿鐨勫悓姝ヨ繃绋嬨 + */ +public class GTaskManager { + private static final String TAG = GTaskManager.class.getSimpleName(); + // 鍚屾鐘舵佸父閲 + public static final int STATE_SUCCESS = 0; + + public static final int STATE_NETWORK_ERROR = 1; + + public static final int STATE_INTERNAL_ERROR = 2; + + public static final int STATE_SYNC_IN_PROGRESS = 3; + + public static final int STATE_SYNC_CANCELLED = 4; + // 鍗曚緥瀹炰緥 + private static GTaskManager mInstance = null; + // 鐢ㄤ簬鑾峰彇璁よ瘉浠ょ墝鐨勬椿鍔ㄤ笂涓嬫枃 + private Activity mActivity; + // 搴旂敤绋嬪簭涓婁笅鏂 + private Context mContext; + // 鍐呭瑙f瀽鍣ㄧ敤浜庢暟鎹簱鎿嶄綔 + private ContentResolver mContentResolver; + // 鍚屾鐘舵佹爣蹇 + private boolean mSyncing; + + private boolean mCancelled; + // 鐢ㄤ簬绠$悊浠诲姟鍒楄〃銆佽妭鐐广佸厓鏁版嵁鍜 GID 涓 NID 鏄犲皠鐨勫搱甯岃〃 + private HashMap mGTaskListHashMap; + + private HashMap mGTaskHashMap; + + private HashMap mMetaHashMap; + + private TaskList mMetaList; + + private HashSet mLocalDeleteIdMap; + + private HashMap mGidToNid; + + private HashMap mNidToGid; + + private GTaskManager() { + // 鍒濆鍖栧悓姝ョ姸鎬佹爣蹇楋紝琛ㄧず褰撳墠娌℃湁姝e湪杩涜鍚屾 + mSyncing = false; + // 鍒濆鍖栧彇娑堝悓姝ユ爣蹇楋紝琛ㄧず褰撳墠娌℃湁鍙栨秷鍚屾 + mCancelled = false; + // 鍒濆鍖栫敤浜庡瓨鍌 Google Task 鍒楄〃鐨勫搱甯岃〃 + mGTaskListHashMap = new HashMap();銆 + // 鍒濆鍖栫敤浜庡瓨鍌 Google Task 鑺傜偣鐨勫搱甯岃〃 + mGTaskHashMap = new HashMap(); + // 鍒濆鍖栫敤浜庡瓨鍌ㄥ厓鏁版嵁鐨勫搱甯岃〃 + mMetaHashMap = new HashMap(); + // 鍒濆鍖栧厓鏁版嵁鍒楄〃锛屽垵濮嬪间负 null + mMetaList = null; + // 鍒濆鍖栫敤浜庡瓨鍌ㄦ湰鍦板垹闄ょ殑绗旇 ID 鐨勯泦鍚 + mLocalDeleteIdMap = new HashSet(); + // 鍒濆鍖栫敤浜庡瓨鍌 Google Task ID 鍒版湰鍦扮瑪璁 ID 鏄犲皠鐨勫搱甯岃〃 + mGidToNid = new HashMap(); + // 鍒濆鍖栫敤浜庡瓨鍌ㄦ湰鍦扮瑪璁 ID 鍒 Google Task ID 鏄犲皠鐨勫搱甯岃〃 + mNidToGid = new HashMap(); + } +/* + * 鑾峰彇 GTaskManager 鐨勫崟渚嬪疄渚嬨 + * 璇ユ柟娉曚娇鐢 synchronized 鍏抽敭瀛楃‘淇濈嚎绋嬪畨鍏紝闃叉澶氫釜绾跨▼鍚屾椂鍒涘缓瀹炰緥銆 + * @return GTaskManager 鐨勫崟渚嬪疄渚 + */ + public static synchronized GTaskManager getInstance() { + // 妫鏌ュ崟渚嬪疄渚嬫槸鍚﹀凡缁忓垱寤 + if (mInstance == null) { + // 濡傛灉鏈垱寤猴紝鍒欏垱寤轰竴涓柊鐨 GTaskManager 瀹炰緥 + mInstance = new GTaskManager(); + } + return mInstance; // 杩斿洖鍗曚緥瀹炰緥 + } +/* + * 璁剧疆娲诲姩涓婁笅鏂囷紝鐢ㄤ簬鑾峰彇璁よ瘉浠ょ墝銆 + * 璇ユ柟娉曚娇鐢 synchronized 鍏抽敭瀛楃‘淇濈嚎绋嬪畨鍏紝闃叉澶氫釜绾跨▼鍚屾椂淇敼 mActivity 鍙橀噺銆 + * @param activity 娲诲姩涓婁笅鏂 + */ + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + // 灏嗕紶鍏ョ殑娲诲姩涓婁笅鏂囪祴鍊肩粰 mActivity 鍙橀噺 + mActivity = activity; + } +/* + * 鍚屾鏈湴绗旇鏁版嵁搴撲笌 Google Tasks銆 + * 璇ユ柟娉曡礋璐e垵濮嬪寲鍚屾杩囩▼锛屽鐞嗗悓姝ヨ繃绋嬩腑鐨勫悇绉嶅紓甯革紝骞惰繑鍥炲悓姝ョ粨鏋滅姸鎬併 + * @param context 搴旂敤绋嬪簭涓婁笅鏂 + * @param asyncTask 寮傛浠诲姟瀵硅薄锛岀敤浜庡彂甯冨悓姝ヨ繘搴 + * @return 鍚屾缁撴灉鐘舵侊紝鍙兘鐨勫间负 STATE_SUCCESS, STATE_NETWORK_ERROR, STATE_INTERNAL_ERROR, STATE_SYNC_IN_PROGRESS, STATE_SYNC_CANCELLED + */ + public int sync(Context context, GTaskASyncTask asyncTask) { + // 妫鏌ユ槸鍚﹀凡缁忔湁鍚屾姝e湪杩涜 + if (mSyncing) { + Log.d(TAG, "Sync is in progress"); + return STATE_SYNC_IN_PROGRESS; + } + // 璁剧疆搴旂敤绋嬪簭涓婁笅鏂囧拰鍐呭瑙f瀽鍣 + mContext = context; + mContentResolver = mContext.getContentResolver(); + // 鏍囪鍚屾姝e湪杩涜 + mSyncing = true; + mCancelled = false; + // 娓呯┖鎵鏈夋暟鎹粨鏋 + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + + try { + // 鑾峰彇 GTaskClient 瀹炰緥骞堕噸缃洿鏂版暟缁 + GTaskClient client = GTaskClient.getInstance(); + client.resetUpdateArray(); + + // login google task + // 鐧诲綍 Google Task + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // get the task list from google + // 浠 Google 鑾峰彇浠诲姟鍒楄〃 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // do content sync work + // 鎵ц鍐呭鍚屾宸ヤ綔 + 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; // 杩斿洖鍚屾缁撴灉鐘舵 + } +/* + * 鍒濆鍖 Google Task 鍒楄〃銆 + * 璇ユ柟娉曚粠 Google Task 鑾峰彇浠诲姟鍒楄〃锛屽苟鍒濆鍖栧厓鏁版嵁鍒楄〃鍜屼换鍔″垪琛ㄣ + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void initGTaskList() throws NetworkFailureException { + // 妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝 + if (mCancelled) + return; + // 鑾峰彇 GTaskClient 瀹炰緥 + GTaskClient client = GTaskClient.getInstance(); + try { + // 浠 Google Task 鑾峰彇浠诲姟鍒楄〃 + JSONArray jsTaskLists = client.getTaskLists(); + + // init meta list first + // 棣栧厛鍒濆鍖栧厓鏁版嵁鍒楄〃 + mMetaList = null; //鍒濆鍖栧厓鏁版嵁鍒楄〃锛屽皢鍏惰缃负 null銆 + for (int i = 0; i < jsTaskLists.length(); i++) { + //閬嶅巻浠诲姟鍒楄〃锛岄愪釜澶勭悊姣忎釜浠诲姟鍒楄〃銆 + JSONObject object = jsTaskLists.getJSONObject(i); + //鑾峰彇褰撳墠浠诲姟鍒楄〃鐨 JSONObject + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + //鑾峰彇褰撳墠浠诲姟鍒楄〃鐨 Google Task ID + //鑾峰彇褰撳墠浠诲姟鍒楄〃鐨勫悕绉般 + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + // 妫鏌ユ槸鍚︽槸鍏冩暟鎹垪琛 + //濡傛灉鏄紝鍒欏垵濮嬪寲鍏冩暟鎹垪琛ㄥ苟鍔犺浇鍏冩暟鎹 + if (name + .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + //鍒涘缓涓涓柊鐨 TaskList 瀵硅薄浣滀负鍏冩暟鎹垪琛ㄣ + mMetaList.setContentByRemoteJSON(object); + //浣跨敤杩滅▼ JSON 鏁版嵁璁剧疆鍏冩暟鎹垪琛ㄧ殑鍐呭銆 + + // load meta data + // 鍔犺浇鍏冩暟鎹 + JSONArray jsMetas = client.getTaskList(gid); + //鑾峰彇鍏冩暟鎹垪琛ㄤ腑鐨勫厓鏁版嵁锛岃繑鍥炰竴涓 JSONArray銆 + for (int j = 0; j < jsMetas.length(); j++) { + //閬嶅巻鍏冩暟鎹垪琛紝閫愪釜澶勭悊姣忎釜鍏冩暟鎹 + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + //鍒涘缓涓涓柊鐨 MetaData 瀵硅薄 + metaData.setContentByRemoteJSON(object); + //浣跨敤杩滅▼ JSON 鏁版嵁璁剧疆鍏冩暟鎹殑鍐呭 + if (metaData.isWorthSaving()) { + //妫鏌ュ厓鏁版嵁鏄惁鍊煎緱淇濆瓨銆傚鏋滃煎緱淇濆瓨锛屽垯灏嗗叾娣诲姞鍒板厓鏁版嵁鍒楄〃涓紝骞舵洿鏂 mMetaHashMap + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + //妫鏌ュ厓鏁版嵁鐨 Google Task ID 鏄惁瀛樺湪銆傚鏋滃瓨鍦紝鍒欏皢鍏舵坊鍔犲埌 mMetaHashMap 涓 + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // create meta list if not existed + // 濡傛灉鍏冩暟鎹垪琛ㄤ笉瀛樺湪锛屽垯鍒涘缓涓涓柊鐨勫厓鏁版嵁鍒楄〃 + if (mMetaList == null) { + //妫鏌ュ厓鏁版嵁鍒楄〃鏄惁涓虹┖銆傚鏋滀负绌猴紝鍒欏垱寤轰竴涓柊鐨勫厓鏁版嵁鍒楄〃骞跺皢鍏舵坊鍔犲埌 Google Task 涓 + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META); + //璁剧疆鍏冩暟鎹垪琛ㄧ殑鍚嶇О + GTaskClient.getInstance().createTaskList(mMetaList); + //灏嗗厓鏁版嵁鍒楄〃娣诲姞鍒 Google Task 涓 + } + + // init task list + // 鍒濆鍖栦换鍔″垪琛 + for (int i = 0; i < jsTaskLists.length(); i++) { + //鍐嶆閬嶅巻浠诲姟鍒楄〃锛岄愪釜澶勭悊姣忎釜浠诲姟鍒楄〃 + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + // 妫鏌ユ槸鍚︽槸 MIUI 鏂囦欢澶瑰墠缂鐨勪换鍔″垪琛 + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) + && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META)) { + //妫鏌ュ綋鍓嶄换鍔″垪琛ㄦ槸鍚︽槸 MIUI 鏂囦欢澶瑰墠缂鐨勪换鍔″垪琛紝骞朵笖涓嶆槸鍏冩暟鎹垪琛ㄣ傚鏋滄槸锛屽垯鍒濆鍖栦换鍔″垪琛ㄥ苟鍔犺浇浠诲姟 + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // load tasks + // 鍔犺浇浠诲姟 + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + //閬嶅巻浠诲姟鍒楄〃锛岄愪釜澶勭悊姣忎釜浠诲姟銆 + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + //妫鏌ヤ换鍔℃槸鍚﹀煎緱淇濆瓨銆傚鏋滃煎緱淇濆瓨锛屽垯灏嗗叾娣诲姞鍒颁换鍔″垪琛ㄤ腑锛屽苟鏇存柊 mGTaskHashMap銆 + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + // 澶勭悊 JSON 瑙f瀽寮傚父 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + } + } +/* + * 鍚屾鍐呭銆 + * 璇ユ柟娉曡礋璐e悓姝ユ湰鍦版暟鎹簱涓殑绗旇涓 Google Tasks 涓殑浠诲姟銆 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + // 娓呯┖鏈湴鍒犻櫎鐨勭瑪璁 ID 闆嗗悎 + mLocalDeleteIdMap.clear(); + // 妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + if (mCancelled) { + return; + } + + // for local deleted note + // 澶勭悊鏈湴鍒犻櫎鐨勭瑪璁 + //浣跨敤 try-finally 缁撴瀯纭繚鍦ㄦ煡璇㈢粨鏉熷悗鍏抽棴娓告爣銆 + 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); + //鏌ヨ鏈湴鍒犻櫎鐨勭瑪璁般傛煡璇㈡潯浠朵负 type 涓嶇瓑浜 Notes.TYPE_SYSTEM 涓 parent_id 绛変簬 Notes.ID_TRASH_FOLER銆 + if (c != null) { + //妫鏌ユ煡璇㈢粨鏋滄槸鍚︿负绌恒傚鏋滀笉涓虹┖锛屽垯閬嶅巻鏌ヨ缁撴灉銆 + while (c.moveToNext()) { + //閬嶅巻鏌ヨ缁撴灉锛岄愪釜澶勭悊姣忎釜绗旇銆 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //鑾峰彇褰撳墠绗旇鐨 Google Task ID銆 + node = mGTaskHashMap.get(gid); + //浠 mGTaskHashMap 涓幏鍙栧搴旂殑鑺傜偣銆 + if (node != null) { + //妫鏌ヨ妭鐐规槸鍚﹀瓨鍦ㄣ傚鏋滃瓨鍦紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣锛屽苟鎵ц杩滅▼鍒犻櫎鎿嶄綔銆 + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); + //鎵ц杩滅▼鍒犻櫎鎿嶄綔銆 + } + + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + //灏嗗綋鍓嶇瑪璁扮殑鏈湴 ID 娣诲姞鍒 mLocalDeleteIdMap 涓 + } + } else { + Log.w(TAG, "failed to query trash folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // sync folder first + syncFolder(); + + // for note existing in database + // 澶勭悊鏁版嵁搴撲腑瀛樺湪鐨勭瑪璁 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + //鏌ヨ鏁版嵁搴撲腑瀛樺湪鐨勭瑪璁般傛煡璇㈡潯浠朵负 type 绛変簬 Notes.TYPE_NOTE 涓 parent_id 涓嶇瓑浜 Notes.ID_TRASH_FOLER銆 + if (c != null) { + //妫鏌ユ煡璇㈢粨鏋滄槸鍚︿负绌恒傚鏋滀笉涓虹┖锛屽垯閬嶅巻鏌ヨ缁撴灉銆 + while (c.moveToNext()) { + //閬嶅巻鏌ヨ缁撴灉锛岄愪釜澶勭悊姣忎釜绗旇銆 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //鑾峰彇褰撳墠绗旇鐨 Google Task ID銆 + node = mGTaskHashMap.get(gid); + //浠 mGTaskHashMap 涓幏鍙栧搴旂殑鑺傜偣銆 + if (node != null) { + //妫鏌ヨ妭鐐规槸鍚﹀瓨鍦ㄣ傚鏋滃瓨鍦紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣锛屽苟鏇存柊 mGidToNid 鍜 mNidToGid 鏄犲皠锛岀劧鍚庤幏鍙栧悓姝ユ搷浣滅被鍨嬨 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + //鏇存柊 Google Task ID 鍒版湰鍦扮瑪璁 ID 鐨勬槧灏勩 + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + //鏇存柊鏈湴绗旇 ID 鍒 Google Task ID 鐨勬槧灏勩 + syncType = node.getSyncAction(c); + //鑾峰彇褰撳墠绗旇鐨勫悓姝ユ搷浣滅被鍨嬨 + } else { + //濡傛灉鑺傜偣涓嶅瓨鍦紝鍒欐牴鎹 Google Task ID 鏄惁涓虹┖鏉ョ‘瀹氬悓姝ユ搷浣滅被鍨嬨 + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + // 鏈湴娣诲姞 + //妫鏌 Google Task ID 鏄惁涓虹┖銆傚鏋滀负绌猴紝鍒欒〃绀烘湰鍦版坊鍔犳搷浣溿 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + //璁剧疆鍚屾鎿嶄綔绫诲瀷涓鸿繙绋嬫坊鍔犮 + } else { + // remote delete + // 杩滅▼鍒犻櫎 + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c);//鎵ц鍚屾鎿嶄綔銆 + } + } else { + Log.w(TAG, "failed to query existing note in database"); + } + + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // go through remaining items + // 澶勭悊鍓╀綑鐨勯」鐩 + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + //鑾峰彇 mGTaskHashMap 鐨勮凯浠e櫒銆 + while (iter.hasNext()) { + //閬嶅巻 mGTaskHashMap锛岄愪釜澶勭悊鍓╀綑鐨勯」鐩 + Map.Entry entry = iter.next(); + //鑾峰彇褰撳墠鐨勯敭鍊煎銆 + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + //鎵ц鏈湴娣诲姞鎿嶄綔 + } + + // mCancelled can be set by another thread, so we neet to check one by + // one + // clear local delete table + // 妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝 + if (!mCancelled) { + // 鎵归噺鍒犻櫎鏈湴鍒犻櫎鐨勭瑪璁 + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + //鎵归噺鍒犻櫎鏈湴鍒犻櫎鐨勭瑪璁般傚鏋滃垹闄ゅけ璐ワ紝鍒欐姏鍑 ActionFailureException銆 + } + + // refresh local sync id + // 鍒锋柊鏈湴鍚屾 ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } +/* + * 鍚屾鏂囦欢澶广 + * 璇ユ柟娉曡礋璐e悓姝ユ湰鍦版暟鎹簱涓殑鏂囦欢澶逛笌 Google Tasks 涓殑浠诲姟鍒楄〃銆 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + //妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + if (mCancelled) { + return; + } + + // for root folder + //澶勭悊鏍规枃浠跺す銆 + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + //鏌ヨ鏍规枃浠跺す銆傛煡璇㈡潯浠朵负 _id 绛変簬 Notes.ID_ROOT_FOLDER銆 + if (c != null) { + //妫鏌ユ煡璇㈢粨鏋滄槸鍚︿负绌恒傚鏋滀笉涓虹┖锛屽垯澶勭悊鏌ヨ缁撴灉銆 + c.moveToNext(); + //绉诲姩娓告爣鍒颁笅涓琛屻 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //鑾峰彇褰撳墠鏂囦欢澶圭殑 Google Task ID銆 + node = mGTaskHashMap.get(gid); + //浠 mGTaskHashMap 涓幏鍙栧搴旂殑鑺傜偣銆 + if (node != null) { + //妫鏌ヨ妭鐐规槸鍚﹀瓨鍦ㄣ傚鏋滃瓨鍦紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣锛屽苟鏇存柊 mGidToNid 鍜 mNidToGid 鏄犲皠銆 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + //鏇存柊 Google Task ID 鍒版湰鍦扮瑪璁 ID 鐨勬槧灏勩 + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // for system folder, only update remote name if necessary + //鏇存柊鏈湴绗旇 ID 鍒 Google Task ID 鐨勬槧灏勩 + 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; + } + } + + // for call-note folder + //澶勭悊閫氳瘽璁板綍鏂囦欢澶广 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + //鏌ヨ閫氳瘽璁板綍鏂囦欢澶广傛煡璇㈡潯浠朵负 _id 绛変簬 Notes.ID_CALL_RECORD_FOLDER銆 + if (c != null) { + //妫鏌ユ煡璇㈢粨鏋滄槸鍚︿负绌恒傚鏋滀笉涓虹┖锛屽垯澶勭悊鏌ヨ缁撴灉銆 + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + //妫鏌ヨ妭鐐规槸鍚﹀瓨鍦ㄣ傚鏋滃瓨鍦紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣锛屽苟鏇存柊 mGidToNid 鍜 mNidToGid 鏄犲皠銆 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); + // for system folder, only update remote name if + // necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE)) + 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; + } + } + + // for local existing folders + //澶勭悊鏈湴瀛樺湪鐨勬枃浠跺す銆 + 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"); + //鏌ヨ鏈湴瀛樺湪鐨勬枃浠跺す銆傛煡璇㈡潯浠朵负 type 绛変簬 Notes.TYPE_FOLDER 涓 parent_id 涓嶇瓑浜 Notes.ID_TRASH_FOLER銆 + if (c != null) { + //妫鏌ユ煡璇㈢粨鏋滄槸鍚︿负绌恒傚鏋滀笉涓虹┖锛屽垯閬嶅巻鏌ヨ缁撴灉銆 + while (c.moveToNext()) { + //閬嶅巻鏌ヨ缁撴灉锛岄愪釜澶勭悊姣忎釜鏂囦欢澶广 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + //妫鏌ヨ妭鐐规槸鍚﹀瓨鍦ㄣ傚鏋滃瓨鍦紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣锛屽苟鏇存柊 mGidToNid 鍜 mNidToGid 鏄犲皠锛岀劧鍚庤幏鍙栧悓姝ユ搷浣滅被鍨嬨 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + //濡傛灉鑺傜偣涓嶅瓨鍦紝鍒欐牴鎹 Google Task ID 鏄惁涓虹┖鏉ョ‘瀹氬悓姝ユ搷浣滅被鍨嬨 + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + //妫鏌 Google Task ID 鏄惁涓虹┖銆傚鏋滀负绌猴紝鍒欒〃绀烘湰鍦版坊鍔犳搷浣 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + //濡傛灉 Google Task ID 涓嶄负绌猴紝鍒欒〃绀鸿繙绋嬪垹闄ゆ搷浣溿 + // remote delete + 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; + } + } + + // for remote add folders + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + //鑾峰彇 mGTaskListHashMap 鐨勮凯浠e櫒銆 + while (iter.hasNext()) { + //閬嶅巻 mGTaskListHashMap锛岄愪釜澶勭悊杩滅▼娣诲姞鐨勬枃浠跺す銆 + Map.Entry entry = iter.next(); + //鑾峰彇褰撳墠鐨勯敭鍊煎 + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + //妫鏌 mGTaskHashMap 涓槸鍚﹀寘鍚鑺傜偣銆傚鏋滃寘鍚紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣锛屽苟鎵ц鏈湴娣诲姞鎿嶄綔銆 + 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: + // 鏈湴鍒犻櫎鎿嶄綔銆備粠 mMetaHashMap 涓幏鍙栧厓鏁版嵁銆傚鏋滃厓鏁版嵁瀛樺湪锛屽垯璋冪敤 GTaskClient.getInstance().deleteNode(meta) 鏂规硶鍒犻櫎鍏冩暟鎹傚皢鏈湴绗旇 ID 娣诲姞鍒 mLocalDeleteIdMap 涓 + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + break; + case Node.SYNC_ACTION_DEL_REMOTE: + // 杩滅▼鍒犻櫎鎿嶄綔銆備粠 mMetaHashMap 涓幏鍙栧厓鏁版嵁銆傚鏋滃厓鏁版嵁瀛樺湪锛屽垯璋冪敤 GTaskClient.getInstance().deleteNode(meta) 鏂规硶鍒犻櫎鍏冩暟鎹傝皟鐢 GTaskClient.getInstance().deleteNode(node) 鏂规硶鍒犻櫎鑺傜偣銆 + 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: + // merging both modifications maybe a good idea + // right now just use local update simply + // 鏇存柊鍐茬獊鎿嶄綔 + // 鍚堝苟涓や釜淇敼鍙兘鏄竴涓ソ涓绘剰 + // 鐜板湪鍙槸绠鍗曞湴浣跨敤鏈湴鏇存柊 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: + // 鏃犳搷浣 + break; + case Node.SYNC_ACTION_ERROR: + default: + // 鏈煡鍚屾鎿嶄綔绫诲瀷 + throw new ActionFailureException("unkown sync action type"); + } + } +/* + * 娣诲姞鏈湴鑺傜偣銆 + * 璇ユ柟娉曡礋璐e皢杩滅▼鑺傜偣娣诲姞鍒版湰鍦版暟鎹簱涓 + * @param node 杩滅▼鑺傜偣瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + //妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + return; + } + + SqlNote sqlNote;//澹版槑涓涓 SqlNote 鍙橀噺锛岀敤浜庡瓨鍌ㄦ湰鍦扮瑪璁般 + if (node instanceof TaskList) { + //妫鏌ヨ妭鐐规槸鍚︽槸浠诲姟鍒楄〃銆傚鏋滄槸浠诲姟鍒楄〃锛屽垯鏍规嵁鑺傜偣鐨勫悕绉板垱寤虹浉搴旂殑鏈湴绗旇銆 + if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { + //妫鏌ヨ妭鐐瑰悕绉版槸鍚︽槸鏍规枃浠跺す銆傚鏋滄槸鏍规枃浠跺す锛屽垯鍒涘缓鏍规枃浠跺す鐨勬湰鍦扮瑪璁般 + sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); + } else if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { + //妫鏌ヨ妭鐐瑰悕绉版槸鍚︽槸閫氳瘽璁板綍鏂囦欢澶广傚鏋滄槸閫氳瘽璁板綍鏂囦欢澶癸紝鍒欏垱寤洪氳瘽璁板綍鏂囦欢澶圭殑鏈湴绗旇銆 + sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); + } else { + //鍏朵粬鏂囦欢澶广傚垱寤烘柊鐨勬湰鍦扮瑪璁帮紝骞惰缃唴瀹瑰拰鐖惰妭鐐 ID銆 + sqlNote = new SqlNote(mContext); + sqlNote.setContent(node.getLocalJSONFromContent()); + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + } + } else { + //濡傛灉鏄换鍔°傚垱寤烘柊鐨勬湰鍦扮瑪璁帮紝骞惰缃唴瀹瑰拰鐖惰妭鐐 ID銆 + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + //澶勭悊 JSON 瑙f瀽寮傚父銆 + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + //妫鏌ユ槸鍚﹀寘鍚瑪璁颁俊鎭傚鏋滃寘鍚瑪璁颁俊鎭紝鍒欐鏌ョ瑪璁 ID 鏄惁鍙敤銆 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + //妫鏌ョ瑪璁颁俊鎭槸鍚﹀寘鍚 ID銆傚鏋滃寘鍚 ID锛屽垯妫鏌 ID 鏄惁鍙敤銆 + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // the id is not available, have to create a new one + //妫鏌ョ瑪璁 ID 鏄惁瀛樺湪浜庢湰鍦版暟鎹簱涓傚鏋滃瓨鍦紝鍒欑Щ闄ょ瑪璁 ID銆 + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + //妫鏌ユ槸鍚﹀寘鍚暟鎹俊鎭傚鏋滃寘鍚暟鎹俊鎭紝鍒欐鏌ユ暟鎹 ID 鏄惁鍙敤銆 + 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)) { + //妫鏌ユ暟鎹俊鎭槸鍚﹀寘鍚 ID銆傚鏋滃寘鍚 ID锛屽垯妫鏌 ID 鏄惁鍙敤銆 + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // the data id is not available, have to create + // a new one + //妫鏌ユ暟鎹 ID 鏄惁瀛樺湪浜庢湰鍦版暟鎹簱涓傚鏋滃瓨鍦紝鍒欑Щ闄ゆ暟鎹 ID銆 + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js);//璁剧疆鏈湴绗旇鐨勫唴瀹广 + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + //鑾峰彇鐖惰妭鐐圭殑鏈湴 ID銆 + if (parentId == null) { + //妫鏌ョ埗鑺傜偣鐨勬湰鍦 ID 鏄惁瀛樺湪銆傚鏋滀笉瀛樺湪锛屽垯鎶涘嚭 ActionFailureException 寮傚父銆 + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // create the local node + sqlNote.setGtaskId(node.getGid());//鏇存柊 Google Task ID 鍒版湰鍦扮瑪璁 ID 鐨勬槧灏勩 + sqlNote.commit(false); + + // update gid-nid mapping + mGidToNid.put(node.getGid(), sqlNote.getId());//鏇存柊鏈湴绗旇 ID 鍒 Google Task ID 鐨勬槧灏勩 + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote);//鏇存柊鍏冩暟鎹 + } +/* + * 鏇存柊鏈湴鑺傜偣銆 + * 璇ユ柟娉曡礋璐e皢杩滅▼鑺傜偣鐨勬洿鏂板悓姝ュ埌鏈湴鏁版嵁搴撲腑銆 + * @param node 杩滅▼鑺傜偣瀵硅薄 + * @param c 娓告爣瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + return; + } + + SqlNote sqlNote; + // update the note locally + //澹版槑涓涓 SqlNote 鍙橀噺锛岀敤浜庡瓨鍌ㄦ湰鍦扮瑪璁般 + sqlNote = new SqlNote(mContext, c); + //鍒涘缓涓涓柊鐨 SqlNote 瀵硅薄锛屽苟浣跨敤娓告爣 c 鍒濆鍖栥 + sqlNote.setContent(node.getLocalJSONFromContent()); + //璁剧疆鏈湴绗旇鐨勫唴瀹逛负杩滅▼鑺傜偣鐨勬湰鍦 JSON 鍐呭銆 + Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + //鑾峰彇鐖惰妭鐐圭殑鏈湴 ID銆傚鏋滆妭鐐规槸浠诲姟锛屽垯浠 mGidToNid 鏄犲皠涓幏鍙栫埗鑺傜偣鐨勬湰鍦 ID銆傚鏋滆妭鐐逛笉鏄换鍔★紝鍒欏皢鐖惰妭鐐 ID 璁剧疆涓烘牴鏂囦欢澶圭殑 ID銆 + if (parentId == null) { + //妫鏌ョ埗鑺傜偣鐨勬湰鍦 ID 鏄惁瀛樺湪銆傚鏋滀笉瀛樺湪锛屽垯璁板綍閿欒鏃ュ織骞舵姏鍑 ActionFailureException 寮傚父銆 + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue());//璁剧疆鏈湴绗旇鐨勭埗鑺傜偣 ID銆 + sqlNote.commit(true);//鎻愪氦鏈湴绗旇鐨勬洿鏂般 + + // update meta info + //鏇存柊鍏冩暟鎹俊鎭 + updateRemoteMeta(node.getGid(), sqlNote); + } +/* + * 娣诲姞杩滅▼鑺傜偣銆 + * 璇ユ柟娉曡礋璐e皢鏈湴绗旇娣诲姞鍒 Google Tasks 涓 + * @param node 杩滅▼鑺傜偣瀵硅薄 + * @param c 娓告爣瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + //鍒涘缓涓涓柊鐨 SqlNote 瀵硅薄锛屽苟浣跨敤娓告爣 c 鍒濆鍖栥 + Node n; + //澹版槑涓涓 Node 鍙橀噺锛岀敤浜庡瓨鍌ㄨ繙绋嬭妭鐐 + + // update remotely + if (sqlNote.isNoteType()) { + //妫鏌ユ湰鍦扮瑪璁版槸鍚︽槸绗旇绫诲瀷銆傚鏋滄槸绗旇绫诲瀷锛屽垯鍒涘缓涓涓柊鐨 Task 瀵硅薄锛屽苟璁剧疆鍐呭銆 + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + String parentGid = mNidToGid.get(sqlNote.getParentId()); + if (parentGid == null) { + //妫鏌ョ埗浠诲姟鍒楄〃鐨 Google Task ID 鏄惁瀛樺湪銆傚鏋滀笉瀛樺湪锛屽垯璁板綍閿欒鏃ュ織骞舵姏鍑 ActionFailureException 寮傚父銆 + 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; + //灏 Task 瀵硅薄璧嬪肩粰 Node 鍙橀噺銆 + + // add meta + //娣诲姞鍏冩暟鎹 + updateRemoteMeta(task.getGid(), sqlNote); + } else { + //濡傛灉鏄枃浠跺す绫诲瀷銆傚垱寤轰竴涓柊鐨 TaskList 瀵硅薄锛屽苟璁剧疆鍐呭銆 + TaskList tasklist = null; + + // we need to skip folder if it has already existed + String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;//鍒濆鍖栨枃浠跺す鍚嶇О + if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) + //妫鏌ユ湰鍦扮瑪璁扮殑 ID 鏄惁鏄牴鏂囦欢澶圭殑 ID銆傚鏋滄槸鏍规枃浠跺す锛屽垯璁剧疆鏂囦欢澶瑰悕绉颁负榛樿鏂囦欢澶瑰悕绉般 + folderName += GTaskStringUtils.FOLDER_DEFAULT; + else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) + //妫鏌ユ湰鍦扮瑪璁扮殑 ID 鏄惁鏄氳瘽璁板綍鏂囦欢澶圭殑 ID銆傚鏋滄槸閫氳瘽璁板綍鏂囦欢澶癸紝鍒欒缃枃浠跺す鍚嶇О涓洪氳瘽璁板綍鏂囦欢澶瑰悕绉般 + folderName += GTaskStringUtils.FOLDER_CALL_NOTE; + else + //鍏朵粬鏂囦欢澶广傝缃枃浠跺す鍚嶇О涓烘湰鍦扮瑪璁扮殑鐗囨銆 + folderName += sqlNote.getSnippet(); + + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + //閬嶅巻 mGTaskListHashMap锛屾煡鎵惧尮閰嶇殑鏂囦欢澶广 + Map.Entry entry = iter.next(); + String gid = entry.getKey(); + TaskList list = entry.getValue(); + + if (list.getName().equals(folderName)) { + //妫鏌ヤ换鍔″垪琛ㄧ殑鍚嶇О鏄惁涓庢枃浠跺す鍚嶇О鍖归厤銆傚鏋滃尮閰嶏紝鍒欏皢浠诲姟鍒楄〃璧嬪肩粰 tasklist 鍙橀噺锛屽苟浠 mGTaskHashMap 涓Щ闄よ鑺傜偣銆 + tasklist = list; + if (mGTaskHashMap.containsKey(gid)) { + //妫鏌 mGTaskHashMap 涓槸鍚﹀寘鍚鑺傜偣銆傚鏋滃寘鍚紝鍒欎粠 mGTaskHashMap 涓Щ闄よ鑺傜偣銆 + mGTaskHashMap.remove(gid); + } + break; + } + } + + // no match we can add now + if (tasklist == null) { + //濡傛灉娌℃湁鍖归厤鐨勬枃浠跺す锛屽垯鍙互娣诲姞銆傚垱寤轰竴涓柊鐨 TaskList 瀵硅薄锛屽苟璁剧疆鍐呭銆 + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist;//灏 TaskList 瀵硅薄璧嬪肩粰 Node 鍙橀噺銆 + } + + // update local note + sqlNote.setGtaskId(n.getGid());//璁剧疆鏈湴绗旇鐨 Google Task ID + sqlNote.commit(false);//鎻愪氦鏈湴绗旇鐨勬洿鏂般 + sqlNote.resetLocalModified();//閲嶇疆鏈湴绗旇鐨勪慨鏀规爣蹇椼 + sqlNote.commit(true);//閲嶇疆鏈湴绗旇鐨勪慨鏀规爣蹇椼 + + // gid-id mapping + mGidToNid.put(n.getGid(), sqlNote.getId());//鏇存柊 Google Task ID 鍒版湰鍦扮瑪璁 ID 鐨勬槧灏勩 + mNidToGid.put(sqlNote.getId(), n.getGid());//鏇存柊鏈湴绗旇 ID 鍒 Google Task ID 鐨勬槧灏勩 + } +/* + * 鏇存柊杩滅▼鑺傜偣銆 + * 璇ユ柟娉曡礋璐e皢鏈湴绗旇鐨勬洿鏂板悓姝ュ埌 Google Tasks 涓 + * @param node 杩滅▼鑺傜偣瀵硅薄 + * @param c 娓告爣瀵硅薄 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c);//鍒涘缓涓涓柊鐨 SqlNote 瀵硅薄锛屽苟浣跨敤娓告爣 c 鍒濆鍖栥 + + // update remotely + //浣跨敤鏈湴绗旇鐨勫唴瀹规洿鏂拌繙绋嬭妭鐐圭殑鍐呭銆 + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + //灏嗘洿鏂板悗鐨勮妭鐐规坊鍔犲埌 GTaskClient 鐨勬洿鏂板垪琛ㄤ腑銆 + + // update meta + //鏇存柊鍏冩暟鎹 + updateRemoteMeta(node.getGid(), sqlNote); + + // move task if necessary + if (sqlNote.isNoteType()) { + //妫鏌ユ湰鍦扮瑪璁版槸鍚︽槸绗旇绫诲瀷銆傚鏋滄槸绗旇绫诲瀷锛屽垯澶勭悊浠诲姟鐨勭Щ鍔 + Task task = (Task) node; + TaskList preParentList = task.getParent(); + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); + if (curParentGid == null) { + //妫鏌ョ埗浠诲姟鍒楄〃鐨 Google Task ID 鏄惁瀛樺湪銆傚鏋滀笉瀛樺湪锛屽垯璁板綍閿欒鏃ュ織骞舵姏鍑 ActionFailureException 寮傚父銆 + 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); + } + } + + // clear local modified flag + sqlNote.resetLocalModified();//娓呴櫎鏈湴绗旇鐨勪慨鏀规爣蹇椼 + sqlNote.commit(true);//鎻愪氦鏈湴绗旇鐨勬洿鏂般 + } +/* + * 鏇存柊杩滅▼鍏冩暟鎹 + * 璇ユ柟娉曡礋璐e皢鏈湴绗旇鐨勫厓鏁版嵁鍚屾鍒 Google Tasks 涓 + * @param gid Google Task ID + * @param 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銆 + * 璇ユ柟娉曡礋璐e埛鏂版湰鍦扮瑪璁扮殑鍚屾 ID锛岀‘淇濇湰鍦扮瑪璁扮殑鍚屾 ID 涓 Google Tasks 涓殑鏈鏂版暟鎹竴鑷淬 + * @throws NetworkFailureException 濡傛灉缃戠粶鎿嶄綔澶辫触 + */ + private void refreshLocalSyncId() throws NetworkFailureException { + // 妫鏌ユ槸鍚﹀凡缁忓彇娑堝悓姝ャ傚鏋 mCancelled 涓 true锛屽垯鐩存帴杩斿洖锛屼笉鎵ц鍚庣画鎿嶄綔銆 + if (mCancelled) { + return; + } + + // get the latest gtask list + mGTaskHashMap.clear();//娓呯┖ mGTaskHashMap + mGTaskListHashMap.clear();//娓呯┖ mGTaskListHashMap銆 + mMetaHashMap.clear();//娓呯┖ mMetaHashMap銆 + initGTaskList();//鍒濆鍖 Google Task 鍒楄〃銆 + + Cursor c = null;//澹版槑涓涓父鏍囧彉閲 c锛岀敤浜庡瓨鍌ㄦ煡璇㈢粨鏋溿 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id<>?)", new String[] { + //鏌ヨ鏈湴绗旇銆傛煡璇㈡潯浠朵负 type 涓嶇瓑浜 Notes.TYPE_SYSTEM 涓 parent_id 涓嶇瓑浜 Notes.ID_TRASH_FOLER銆 + 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 涓Щ闄よ鑺傜偣锛屽苟鏇存柊鏈湴绗旇鐨勫悓姝 ID銆 + 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 { + //濡傛灉鑺傜偣涓嶅瓨鍦紝鍒欒褰曢敊璇棩蹇楀苟鎶涘嚭 ActionFailureException 寮傚父銆 + Log.e(TAG, "something is missed"); + throw new ActionFailureException( + "some local items don't have gid after sync"); + } + } + } else { + Log.w(TAG, "failed to query local note to refresh sync id"); + } + } finally { + if (c != null) { + //妫鏌ユ父鏍囨槸鍚︿负绌恒傚鏋滀笉涓虹┖锛屽垯鍏抽棴娓告爣銆 + c.close(); + c = null; + } + } + } +/* + * 鑾峰彇鍚屾璐︽埛鍚嶇О銆 + * 璇ユ柟娉曡繑鍥炲綋鍓嶇敤浜庡悓姝ョ殑 Google 璐︽埛鍚嶇О銆 + * @return 鍚屾璐︽埛鍚嶇О + */ + public String getSyncAccount() { + // 鑾峰彇 GTaskClient 鐨勫崟渚嬪疄渚嬶紝骞惰繑鍥炲悓姝ヨ处鎴风殑鍚嶇О + return GTaskClient.getInstance().getSyncAccount().name; + } +/* + * 鍙栨秷鍚屾銆 + * 璇ユ柟娉曠敤浜庤缃彇娑堝悓姝ユ爣蹇楋紝浠ヤ究鍦ㄥ悓姝ヨ繃绋嬩腑鍋滄鍚屾鎿嶄綔銆 + */ + public void cancelSync() { + // 璁剧疆鍙栨秷鍚屾鏍囧織涓 true + mCancelled = true; + } +} -- 2.34.1 From d2f0cccee7093de112552809ec838a9cb67f0502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:19:58 +0800 Subject: [PATCH 23/44] Signed-off-by: --- GTaskSyncService.txt | 198 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 GTaskSyncService.txt diff --git a/GTaskSyncService.txt b/GTaskSyncService.txt new file mode 100644 index 0000000..a20653c --- /dev/null +++ b/GTaskSyncService.txt @@ -0,0 +1,198 @@ +/* + * 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.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +/* + * GTaskSyncService绫绘槸涓涓湇鍔$被锛岀敤浜庡鐞咷oogle浠诲姟鐨勫悓姝ユ搷浣溿 + * 璇ョ被缁ф壙鑷猄ervice锛屽苟鍦ㄥ悗鍙版墽琛屽悓姝ヤ换鍔° + */ +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) { + // 鍒涘缓涓涓柊鐨凣TaskASyncTask瀵硅薄 + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + public void onComplete() { + // 鍚屾浠诲姟瀹屾垚鍚庯紝灏唌SyncTask缃负null + mSyncTask = null; + // 鍙戦佸箍鎾氱煡鍚屾瀹屾垚 + sendBroadcast(""); + // 鍋滄鏈嶅姟 + stopSelf(); + } + }); + // 鍙戦佸箍鎾氱煡鍚屾寮濮 + sendBroadcast(""); + // 鎵ц鍚屾浠诲姟 + mSyncTask.execute(); + } + } +/* + * 鍙栨秷鍚屾鎿嶄綔銆 + */ + private void cancelSync() { + // 濡傛灉鍚屾浠诲姟姝e湪鎵ц + if (mSyncTask != null) { + // 璋冪敤cancelSync鏂规硶鍙栨秷鍚屾 + mSyncTask.cancelSync(); + } + } +/* + * 鏈嶅姟鍒涘缓鏃惰皟鐢紝鍒濆鍖栧悓姝ヤ换鍔° + */ + @Override + public void onCreate() { + // 灏嗗悓姝ヤ换鍔$疆涓簄ull + mSyncTask = null; + } +/* + * 鏈嶅姟鍚姩鏃惰皟鐢紝澶勭悊鍚姩鍜屽彇娑堝悓姝ョ殑鍛戒护銆 + * @param intent 鍚姩鏈嶅姟鐨勬剰鍥 + * @param flags 鍚姩鏍囧織 + * @param startId 鍚姩ID + * @return 鏈嶅姟鍚姩妯″紡 + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // 鑾峰彇鎰忓浘涓殑棰濆鏁版嵁 + Bundle bundle = intent.getExtras(); + // 濡傛灉棰濆鏁版嵁涓嶄负绌轰笖鍖呭惈ACTION_STRING_NAME + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 鏍规嵁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; + } + // 杩斿洖START_STICKY锛岃〃绀烘湇鍔″湪寮傚父缁堟鍚庝細鑷姩閲嶅惎 + return START_STICKY; + } + // 濡傛灉棰濆鏁版嵁涓虹┖鎴栦笉鍖呭惈ACTION_STRING_NAME锛岃皟鐢ㄧ埗绫荤殑onStartCommand鏂规硶 + return super.onStartCommand(intent, flags, startId); + } +/* + * 鍐呭瓨涓嶈冻鏃惰皟鐢紝鍙栨秷鍚屾浠诲姟銆 + */ + @Override + public void onLowMemory() { + // 濡傛灉鍚屾浠诲姟姝e湪鎵ц + if (mSyncTask != null) { + // 璋冪敤cancelSync鏂规硶鍙栨秷鍚屾 + mSyncTask.cancelSync(); + } + } +/* + * 缁戝畾鏈嶅姟鏃惰皟鐢紝杩斿洖null銆 + * @param intent 缁戝畾鏈嶅姟鐨勬剰鍥 + * @return null + */ + public IBinder onBind(Intent intent) { + // 杩斿洖null锛岃〃绀轰笉鏀寔缁戝畾鏈嶅姟 + return null; + } +/* + * 鍙戦佸箍鎾紝閫氱煡鍚屾鐘舵佸拰杩涘害銆 + * @param msg 杩涘害娑堟伅 + */ + public void sendBroadcast(String msg) { + // 鏇存柊鍚屾杩涘害瀛楃涓 + mSyncProgress = msg; + // 鍒涘缓涓涓柊鐨処ntent瀵硅薄锛岀敤浜庡彂閫佸箍鎾 + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + // 娣诲姞鏄惁姝e湪鍚屾鐨勬爣蹇 + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + // 娣诲姞杩涘害娑堟伅 + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + // 鍙戦佸箍鎾 + sendBroadcast(intent); + } +/* + * 鍚姩鍚屾鎿嶄綔鐨勯潤鎬佹柟娉曘 + * @param activity 鍚姩鍚屾鐨凙ctivity + */ + public static void startSync(Activity activity) { + // 璁剧疆娲诲姩涓婁笅鏂 + GTaskManager.getInstance().setActivityContext(activity); + // 鍒涘缓涓涓柊鐨処ntent瀵硅薄锛岀敤浜庡惎鍔ㄦ湇鍔 + Intent intent = new Intent(activity, GTaskSyncService.class); + // 娣诲姞鍚姩鍚屾鐨勫懡浠 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + // 鍚姩鏈嶅姟 + activity.startService(intent); + } +/* + * 鍙栨秷鍚屾鎿嶄綔鐨勯潤鎬佹柟娉曘 + * @param context 鍙栨秷鍚屾鐨勪笂涓嬫枃 + */ + public static void cancelSync(Context context) { + // 鍒涘缓涓涓柊鐨処ntent瀵硅薄锛岀敤浜庡惎鍔ㄦ湇鍔 + Intent intent = new Intent(context, GTaskSyncService.class); + // 娣诲姞鍙栨秷鍚屾鐨勫懡浠 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + // 鍚姩鏈嶅姟 + context.startService(intent); + } +/* + * 妫鏌ユ槸鍚︽鍦ㄥ悓姝ャ + * @return 濡傛灉姝e湪鍚屾锛岃繑鍥瀟rue锛涘惁鍒欒繑鍥瀎alse + */ + public static boolean isSyncing() { + // 濡傛灉mSyncTask涓嶄负null锛岃〃绀烘鍦ㄥ悓姝 + return mSyncTask != null; + } +/* + * 鑾峰彇鍚屾杩涘害瀛楃涓层 + * @return 鍚屾杩涘害瀛楃涓 + */ + public static String getProgressString() { + // 杩斿洖褰撳墠鐨勫悓姝ヨ繘搴﹀瓧绗︿覆 + return mSyncProgress; + } +} -- 2.34.1 From aaaeaa227c6ab42b9e6a599a04cefda3e4508d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:21:50 +0800 Subject: [PATCH 24/44] Signed-off-by: --- Note.txt | 382 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 Note.txt diff --git a/Note.txt b/Note.txt new file mode 100644 index 0000000..7bcca07 --- /dev/null +++ b/Note.txt @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.net.Uri; +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.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; + +import java.util.ArrayList; + +/* + * Note 绫荤敤浜庣鐞嗙瑪璁扮殑鍒涘缓銆佹洿鏂板拰鍚屾銆 + * 璇ョ被鎻愪緵浜嗗垱寤烘柊绗旇銆佽缃瑪璁板唴瀹广佸悓姝ョ瑪璁扮瓑鍔熻兘銆 + */ +public class Note { + private ContentValues mNoteDiffValues; + private NoteData mNoteData; + private static final String TAG = "Note"; + /** + * Create a new note id for adding a new note to databases + */ + /* + * 鍒涘缓涓涓柊鐨勭瑪璁 ID锛岀敤浜庡皢鏂扮瑪璁版坊鍔犲埌鏁版嵁搴撲腑銆 + * + * @param context 搴旂敤绋嬪簭涓婁笅鏂 + * @param folderId 鏂囦欢澶 ID + * @return 鏂板垱寤虹殑绗旇 ID + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // Create a new note in the database + // 鍒涘缓涓涓柊鐨勭瑪璁板埌鏁版嵁搴撲腑 + ContentValues values = new ContentValues(); + //鍒涘缓涓涓柊鐨 ContentValues 瀵硅薄锛岀敤浜庡瓨鍌ㄧ瑪璁扮殑鍒濆鍊笺 + long createdTime = System.currentTimeMillis(); + //鑾峰彇褰撳墠鏃堕棿鎴筹紝浣滀负绗旇鐨勫垱寤烘椂闂村拰淇敼鏃堕棿銆 + values.put(NoteColumns.CREATED_DATE, createdTime); + //灏嗗垱寤烘椂闂村瓨鍌ㄥ埌 ContentValues 瀵硅薄涓 + values.put(NoteColumns.MODIFIED_DATE, createdTime); + //灏嗕慨鏀规椂闂村瓨鍌ㄥ埌 ContentValues 瀵硅薄涓 + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + //灏嗙瑪璁扮被鍨嬪瓨鍌ㄥ埌 ContentValues 瀵硅薄涓 + values.put(NoteColumns.LOCAL_MODIFIED, 1); + //灏嗘湰鍦颁慨鏀规爣蹇楄缃负 1锛岃〃绀虹瑪璁板凡琚慨鏀广 + values.put(NoteColumns.PARENT_ID, folderId); + //灏嗘枃浠跺す ID 瀛樺偍鍒 ContentValues 瀵硅薄涓 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + //灏 ContentValues 瀵硅薄鎻掑叆鍒版暟鎹簱涓紝骞惰繑鍥炴柊鍒涘缓绗旇鐨 URI銆 + long noteId = 0;//鍒濆鍖栫瑪璁 ID 鍙橀噺銆 + try { + //灏濊瘯浠 URI 涓幏鍙栫瑪璁 ID銆傚鏋滃彂鐢 NumberFormatException 寮傚父锛屽垯璁板綍閿欒鏃ュ織锛屽苟灏嗙瑪璁 ID 璁剧疆涓 0銆 + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + //妫鏌ョ瑪璁 ID 鏄惁涓 -1銆傚鏋滀负 -1锛屽垯鎶涘嚭 IllegalStateException 寮傚父銆 + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId;//杩斿洖鏂板垱寤虹殑绗旇 ID銆 + } +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栫瑪璁扮殑宸紓鍊煎拰绗旇鏁版嵁銆 + */ + public Note() { + //// 鍒濆鍖栫瑪璁扮殑宸紓鍊 + mNoteDiffValues = new ContentValues(); + // // 鍒濆鍖栫瑪璁版暟鎹 + mNoteData = new NoteData(); + } +/* + * 璁剧疆绗旇鐨勫樊寮傚笺 + * @param key 閿 + * @param value 鍊 + */ + public void setNoteValue(String key, String value) { + // 灏嗛敭鍊煎瀛樺偍鍒扮瑪璁扮殑宸紓鍊间腑 + mNoteDiffValues.put(key, value); + // 璁剧疆鏈湴淇敼鏍囧織涓 1锛岃〃绀虹瑪璁板凡琚慨鏀 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 璁剧疆淇敼鏃堕棿涓哄綋鍓嶆椂闂 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } +/* + * 璁剧疆绗旇鐨勬枃鏈暟鎹 + * @param key 閿 + * @param value 鍊 + */ + public void setTextData(String key, String value) { + // 璋冪敤 NoteData 绫荤殑 setTextData 鏂规硶锛岃缃枃鏈暟鎹 + mNoteData.setTextData(key, value); + } +/* + * 璁剧疆绗旇鐨勬枃鏈暟鎹 ID銆 + * @param id 鏂囨湰鏁版嵁 ID + */ + public void setTextDataId(long id) { + // 璋冪敤 NoteData 绫荤殑 setTextDataId 鏂规硶锛岃缃枃鏈暟鎹 ID + mNoteData.setTextDataId(id); + } +/* + * 鑾峰彇绗旇鐨勬枃鏈暟鎹 ID銆 + * @return 鏂囨湰鏁版嵁 ID + */ + public long getTextDataId() { + // 杩斿洖 NoteData 绫荤殑 mTextDataId 瀛楁 + return mNoteData.mTextDataId; + } +/* + * 璁剧疆绗旇鐨勯氳瘽鏁版嵁 ID銆 + * @param id 閫氳瘽鏁版嵁 ID + */ + public void setCallDataId(long id) { + // 璋冪敤 NoteData 绫荤殑 setCallDataId 鏂规硶锛岃缃氳瘽鏁版嵁 ID + mNoteData.setCallDataId(id); + } +/* + * 璁剧疆璋冪敤鏁版嵁鐨勬柟娉曘 + * 璇ユ柟娉曟帴鍙椾袱涓弬鏁帮細涓涓敭鍜屼竴涓硷紝骞跺皢瀹冧滑浼犻掔粰 `mNoteData` 瀵硅薄鐨 `setCallData` 鏂规硶銆 + * @param key 鐢ㄤ簬瀛樺偍鏁版嵁鐨勯敭銆 + * @param value 涓庨敭鍏宠仈鐨勫笺 + */ + public void setCallData(String key, String value) { + //璋冪敤 `mNoteData` 瀵硅薄鐨 `setCallData` 鏂规硶锛屽苟灏 `key` 鍜 `value` 浣滀负鍙傛暟浼犻掋 + mNoteData.setCallData(key, value); + } +/* + * 妫鏌ユ槸鍚︽湁鏈湴淇敼鐨勬柟娉曘 + * 璇ユ柟娉曡繑鍥炰竴涓竷灏斿硷紝鎸囩ず鏄惁鏈夋湰鍦颁慨鏀广傚鏋滄湁鏈湴淇敼锛屽垯杩斿洖 true锛涘惁鍒欒繑鍥 false銆 + * 鏈湴淇敼鐨勫垽鏂熀浜庝袱涓潯浠讹細 + * 1. `mNoteDiffValues` 闆嗗悎鐨勫ぇ灏忔槸鍚﹀ぇ浜 0銆 + * 2. `mNoteData` 瀵硅薄鏄惁鎶ュ憡鏈湴淇敼銆 + * @return 濡傛灉鏈夋湰鍦颁慨鏀癸紝鍒欒繑鍥 true锛涘惁鍒欒繑鍥 false銆 + */ + public boolean isLocalModified() { + // 妫鏌 `mNoteDiffValues` 闆嗗悎鐨勫ぇ灏忔槸鍚﹀ぇ浜 0锛屾垨鑰 `mNoteData` 瀵硅薄鏄惁鎶ュ憡鏈湴淇敼銆 + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } +/* + * 鍚屾绗旇鐨勬柟娉曘 + * 璇ユ柟娉曠敤浜庡皢鏈湴淇敼鐨勭瑪璁版暟鎹悓姝ュ埌鍐呭鎻愪緵鑰呬腑銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param noteId 绗旇鐨勫敮涓鏍囪瘑绗︺ + * @return 濡傛灉鍚屾鎴愬姛锛屽垯杩斿洖 true锛涘惁鍒欒繑鍥 false銆 + * @throws IllegalArgumentException 濡傛灉 noteId 灏忎簬鎴栫瓑浜 0锛屽垯鎶涘嚭姝ゅ紓甯搞 + */ + public boolean syncNote(Context context, long noteId) { + /// 妫鏌 noteId 鏄惁鏈夋晥锛屽鏋滄棤鏁堝垯鎶涘嚭寮傚父銆 + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + // 濡傛灉娌℃湁鏈湴淇敼锛屽垯鐩存帴杩斿洖 true銆 + if (!isLocalModified()) { + return true; + } + + /** + * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and + * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the + * note data info + * 鐞嗚涓婏紝涓鏃︽暟鎹彂鐢熷彉鍖栵紝绗旇搴旇鍦 {@link NoteColumns#LOCAL_MODIFIED} 鍜 + * {@link NoteColumns#MODIFIED_DATE} 涓婃洿鏂般備负浜嗘暟鎹畨鍏紝鍗充娇鏇存柊绗旇澶辫触锛屾垜浠篃浼氭洿鏂扮瑪璁版暟鎹俊鎭 + */ + // 灏濊瘯鏇存柊绗旇鏁版嵁銆 + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + // 濡傛灉鏇存柊澶辫触锛岃褰曢敊璇棩蹇椼 + Log.e(TAG, "Update note error, should not happen"); + // Do not return, fall through + // 涓嶈繑鍥烇紝缁х画鎵ц銆 + } + // 娓呯┖ mNoteDiffValues 闆嗗悎銆 + mNoteDiffValues.clear(); + // 濡傛灉 mNoteData 鏈夋湰鍦颁慨鏀癸紝骞朵笖鎺ㄩ佹暟鎹埌鍐呭鎻愪緵鑰呭け璐ワ紝鍒欒繑鍥 false銆 + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; + } + // 濡傛灉鎵鏈夋搷浣滄垚鍔燂紝鍒欒繑鍥 true銆 + return true; + } +/* + * 绗旇鏁版嵁绫伙紝鐢ㄤ簬绠$悊绗旇鐨勬枃鏈暟鎹拰閫氳瘽鏁版嵁銆 + */ + private class NoteData { + private long mTextDataId;// 鏂囨湰鏁版嵁鐨勫敮涓鏍囪瘑绗 + + private ContentValues mTextDataValues;// 鏂囨湰鏁版嵁鐨 ContentValues 瀵硅薄 + + private long mCallDataId; // 閫氳瘽鏁版嵁鐨勫敮涓鏍囪瘑绗 + + private ContentValues mCallDataValues; // 閫氳瘽鏁版嵁鐨 ContentValues 瀵硅薄 + + private static final String TAG = "NoteData";// 鏃ュ織鏍囩 +/* + * 鏋勯犲嚱鏁帮紝鍒濆鍖栨枃鏈暟鎹拰閫氳瘽鏁版嵁鐨 ContentValues 瀵硅薄锛屽苟灏嗘暟鎹 ID 璁剧疆涓 0銆 + */ + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } +/* + * 妫鏌ユ槸鍚︽湁鏈湴淇敼鐨勬柟娉曘 + * @return 濡傛灉鏈夋湰鍦颁慨鏀癸紝鍒欒繑鍥 true锛涘惁鍒欒繑鍥 false銆 + */ + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } +/* + * 璁剧疆鏂囨湰鏁版嵁鐨勫敮涓鏍囪瘑绗︺ + * 璇ユ柟娉曠敤浜庤缃枃鏈暟鎹殑鍞竴鏍囪瘑绗︺傚鏋滀紶鍏ョ殑 id 灏忎簬鎴栫瓑浜 0锛屽垯鎶涘嚭 IllegalArgumentException 寮傚父銆 + * @param id 鏂囨湰鏁版嵁鐨勫敮涓鏍囪瘑绗︺ + * @throws IllegalArgumentException 濡傛灉 id 灏忎簬鎴栫瓑浜 0锛屽垯鎶涘嚭姝ゅ紓甯搞 + */ + void setTextDataId(long id) { + // 妫鏌ヤ紶鍏ョ殑 id 鏄惁鏈夋晥锛屽鏋滄棤鏁堝垯鎶涘嚭寮傚父銆 + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); + } + mTextDataId = id;// 璁剧疆鏂囨湰鏁版嵁鐨勫敮涓鏍囪瘑绗︺ + } +/* + * 璁剧疆閫氳瘽鏁版嵁鐨勫敮涓鏍囪瘑绗︺ + * 璇ユ柟娉曠敤浜庤缃氳瘽鏁版嵁鐨勫敮涓鏍囪瘑绗︺傚鏋滀紶鍏ョ殑 id 灏忎簬鎴栫瓑浜 0锛屽垯鎶涘嚭 IllegalArgumentException 寮傚父銆 + * @param id 閫氳瘽鏁版嵁鐨勫敮涓鏍囪瘑绗︺ + * @throws IllegalArgumentException 濡傛灉 id 灏忎簬鎴栫瓑浜 0锛屽垯鎶涘嚭姝ゅ紓甯搞 + */ + void setCallDataId(long id) { + // 妫鏌ヤ紶鍏ョ殑 id 鏄惁鏈夋晥锛屽鏋滄棤鏁堝垯鎶涘嚭寮傚父銆 + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); + } + mCallDataId = id; // 璁剧疆閫氳瘽鏁版嵁鐨勫敮涓鏍囪瘑绗︺ + } +/* + * 璁剧疆閫氳瘽鏁版嵁鐨勬柟娉曘 + * 璇ユ柟娉曠敤浜庡皢缁欏畾鐨勯敭鍊煎瀛樺偍鍒伴氳瘽鏁版嵁鐨 ContentValues 瀵硅薄涓紝骞舵洿鏂扮瑪璁扮殑鏈湴淇敼鐘舵佸拰淇敼鏃ユ湡銆 + * @param key 閫氳瘽鏁版嵁鐨勯敭銆 + * @param value 閫氳瘽鏁版嵁鐨勫笺 + */ + void setCallData(String key, String value) { + // 灏嗙粰瀹氱殑閿煎瀛樺偍鍒伴氳瘽鏁版嵁鐨 ContentValues 瀵硅薄涓 + mCallDataValues.put(key, value); + // 鏇存柊绗旇鐨勬湰鍦颁慨鏀圭姸鎬佷负 1锛岃〃绀烘湁鏈湴淇敼銆 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 鏇存柊绗旇鐨勪慨鏀规棩鏈熶负褰撳墠鏃堕棿銆 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } +/* + *璁剧疆鏂囨湰鏁版嵁鐨勬柟娉曘 + * 璇ユ柟娉曠敤浜庡皢缁欏畾鐨勯敭鍊煎瀛樺偍鍒版枃鏈暟鎹殑 ContentValues 瀵硅薄涓紝骞舵洿鏂扮瑪璁扮殑鏈湴淇敼鐘舵佸拰淇敼鏃ユ湡銆 + * @param key 鏂囨湰鏁版嵁鐨勯敭銆 + * @param value 鏂囨湰鏁版嵁鐨勫笺 + */ + void setTextData(String key, String value) { + // 灏嗙粰瀹氱殑閿煎瀛樺偍鍒版枃鏈暟鎹殑 ContentValues 瀵硅薄涓 + mTextDataValues.put(key, value); + // 鏇存柊绗旇鐨勬湰鍦颁慨鏀圭姸鎬佷负 1锛岃〃绀烘湁鏈湴淇敼銆 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 鏇存柊绗旇鐨勪慨鏀规棩鏈熶负褰撳墠鏃堕棿銆 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } +/* + * 灏嗘暟鎹帹閫佸埌鍐呭鎻愪緵鑰呯殑鏂规硶銆 + * 璇ユ柟娉曠敤浜庡皢鏂囨湰鏁版嵁鍜岄氳瘽鏁版嵁鎺ㄩ佸埌鍐呭鎻愪緵鑰呬腑銆傚鏋滄枃鏈暟鎹垨閫氳瘽鏁版嵁鏈変慨鏀癸紝鍒欏皢鍏舵彃鍏ユ垨鏇存柊鍒板唴瀹规彁渚涜呬腑銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param noteId 绗旇鐨勫敮涓鏍囪瘑绗︺ + * @return 濡傛灉鎺ㄩ佹垚鍔燂紝鍒欒繑鍥 Uri锛涘惁鍒欒繑鍥 null銆 + * @throws IllegalArgumentException 濡傛灉 noteId 灏忎簬鎴栫瓑浜 0锛屽垯鎶涘嚭姝ゅ紓甯搞 + */ + Uri pushIntoContentResolver(Context context, long noteId) { + // 杩欐槸鏂规硶鐨勭鍚嶏紝瀹氫箟浜嗘柟娉曠殑璁块棶淇グ绗︺佽繑鍥炵被鍨嬨佹柟娉曞悕绉板拰鍙傛暟鍒楄〃銆 + /** + * Check for safety + */ + //妫鏌 noteId 鏄惁鏈夋晥锛屽鏋滄棤鏁堝垯鎶涘嚭 + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + //鍒涘缓涓涓搷浣滃垪琛紝鐢ㄤ簬鎵归噺鎿嶄綔銆 + ArrayList operationList = new ArrayList(); + ContentProviderOperation.Builder builder = null; + //濡傛灉鏂囨湰鏁版嵁鏈変慨鏀癸紝澶勭悊鏂囨湰鏁版嵁銆 + if(mTextDataValues.size() > 0) { + //灏嗙瑪璁 ID 娣诲姞鍒版枃鏈暟鎹殑 ContentValues 瀵硅薄涓 + mTextDataValues.put(DataColumns.NOTE_ID, noteId); + //濡傛灉鏂囨湰鏁版嵁鐨 ID 涓 0锛岃〃绀烘槸鏂版暟鎹紝鎻掑叆鍒板唴瀹规彁渚涜呬腑銆 + 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; + } + } //濡傛灉鏂囨湰鏁版嵁鐨 ID 涓嶄负 0锛岃〃绀烘槸宸叉湁鏁版嵁锛屾洿鏂板埌鍐呭鎻愪緵鑰呬腑銆 + else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear();// 娓呯┖鏂囨湰鏁版嵁鐨 ContentValues 瀵硅薄銆 + } + // 濡傛灉閫氳瘽鏁版嵁鏈変慨鏀癸紝澶勭悊閫氳瘽鏁版嵁銆 + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); + //灏嗙瑪璁 ID 娣诲姞鍒伴氳瘽鏁版嵁鐨 ContentValues 瀵硅薄涓 + //濡傛灉閫氳瘽鏁版嵁鐨 ID 涓 0锛岃〃绀烘槸鏂版暟鎹紝鎻掑叆鍒板唴瀹规彁渚涜呬腑銆 + 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; + } + } //濡傛灉閫氳瘽鏁版嵁鐨 ID 涓嶄负 0锛岃〃绀烘槸宸叉湁鏁版嵁锛屾洿鏂板埌鍐呭鎻愪緵鑰呬腑銆 + else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear();//娓呯┖閫氳瘽鏁版嵁鐨 ContentValues 瀵硅薄銆 + } + //濡傛灉鎿嶄綔鍒楄〃涓湁鎿嶄綔锛屾墽琛屾壒閲忔搷浣溿 + 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 e) { + //鎹曡幏杩滅▼寮傚父骞惰褰曟棩蹇椼 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } catch (OperationApplicationException e) { + // 鎹曡幏鎿嶄綔搴旂敤寮傚父骞惰褰曟棩蹇椼 + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} -- 2.34.1 From 8c2de5e290a3e262c3397baddfec0d11b416c042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 02:23:23 +0800 Subject: [PATCH 25/44] Signed-off-by: --- WorkingNote.txt | 532 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 WorkingNote.txt diff --git a/WorkingNote.txt b/WorkingNote.txt new file mode 100644 index 0000000..8e7a29c --- /dev/null +++ b/WorkingNote.txt @@ -0,0 +1,532 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +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 { + // Note for the working note + // 褰撳墠宸ヤ綔绗旇鐨 Note 瀵硅薄 + private Note mNote; + // Note Id + // 绗旇鐨勫敮涓鏍囪瘑绗 + private long mNoteId; + // Note content + // 绗旇鍐呭 + private String mContent; + // Note mode + // 绗旇妯″紡 + private int mMode; + // 鎻愰啋鏃ユ湡 + private long mAlertDate; + // 淇敼鏃ユ湡 + private long mModifiedDate; + // 鑳屾櫙棰滆壊 ID + private int mBgColorId; + // 灏忛儴浠 ID + private int mWidgetId; + // 灏忛儴浠剁被鍨 + private int mWidgetType; + // 鏂囦欢澶 ID + private long mFolderId; + // 搴旂敤绋嬪簭涓婁笅鏂 + 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,// 鏁版嵁鐨 MIME 绫诲瀷 + DataColumns.DATA1,// 鏁版嵁瀛楁 1 + DataColumns.DATA2, // 鏁版嵁瀛楁 2 + DataColumns.DATA3, // 鏁版嵁瀛楁 3 + DataColumns.DATA4, // 鏁版嵁瀛楁 4 + }; +/* + * 绗旇鏌ヨ鎶曞奖銆 + * 璇ュ父閲忓畾涔変簡鍦ㄦ煡璇㈢瑪璁版椂闇瑕佽繑鍥炵殑鍒椼 + * @see NoteColumns + */ + public static final String[] NOTE_PROJECTION = new String[] { + NoteColumns.PARENT_ID, // 绗旇鐨勭埗 ID锛堟枃浠跺す ID锛 + NoteColumns.ALERTED_DATE, // 绗旇鐨勬彁閱掓棩鏈 + NoteColumns.BG_COLOR_ID, // 绗旇鐨勮儗鏅鑹 ID + NoteColumns.WIDGET_ID, // 绗旇鐨勫皬閮ㄤ欢 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; + + // New note construct + /* + * 鍒涘缓涓涓柊鐨勭┖绗旇銆 + * 璇ユ瀯閫犲嚱鏁扮敤浜庡垵濮嬪寲涓涓柊鐨勭┖绗旇瀵硅薄銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param folderId 鏂囦欢澶 ID銆 + */ + private WorkingNote(Context context, long folderId) { + // 璁剧疆搴旂敤绋嬪簭涓婁笅鏂 + mContext = context; + mAlertDate = 0; // 鍒濆鍖栨彁閱掓棩鏈熶负 0 + mModifiedDate = System.currentTimeMillis();// 璁剧疆淇敼鏃ユ湡涓哄綋鍓嶆椂闂 + mFolderId = folderId;// 璁剧疆鏂囦欢澶 ID + mNote = new Note();// 鍒涘缓涓涓柊鐨 Note 瀵硅薄 + mNoteId = 0;// 鍒濆鍖栫瑪璁 ID 涓 0 + mIsDeleted = false;// 鍒濆鍖栧垹闄ょ姸鎬佷负 false + mMode = 0;// 鍒濆鍖栫瑪璁版ā寮忎负 0 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE;// 鍒濆鍖栧皬閮ㄤ欢绫诲瀷涓烘棤鏁堢被鍨 + } + + // Existing note construct + /* + * 鍔犺浇鐜版湁鐨勭瑪璁 + * 璇ユ瀯閫犲嚱鏁扮敤浜庡姞杞界幇鏈夌殑绗旇瀵硅薄銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param noteId 绗旇鐨勫敮涓鏍囪瘑绗︺ + * @param folderId 鏂囦欢澶 ID銆 + */ + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context;// 璁剧疆搴旂敤绋嬪簭涓婁笅鏂 + mNoteId = noteId;// 璁剧疆绗旇鐨勫敮涓鏍囪瘑绗 + mFolderId = folderId;// 璁剧疆鏂囦欢澶 ID + mIsDeleted = false;// 鍒濆鍖栧垹闄ょ姸鎬佷负 false + mNote = new Note(); // 鍒涘缓涓涓柊鐨 Note 瀵硅薄 + loadNote();// 鍔犺浇绗旇鏁版嵁 + } +/* + * 鍔犺浇绗旇鏁版嵁銆 + * 璇ユ柟娉曠敤浜庝粠鍐呭鎻愪緵鑰呬腑鍔犺浇绗旇鏁版嵁锛屽苟灏嗗叾璁剧疆鍒板綋鍓嶇殑 WorkingNote 瀵硅薄涓 + */ + private void loadNote() { + // 鏌ヨ绗旇鏁版嵁 + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); // 妫鏌ユ父鏍囨槸鍚︿负绌 + + if (cursor != null) { // 绉诲姩娓告爣鍒扮涓琛 + if (cursor.moveToFirst()) { + mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); // 鑾峰彇鏂囦欢澶 ID + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); // 鑾峰彇鑳屾櫙棰滆壊 ID + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);// 鑾峰彇灏忛儴浠 ID + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); // 鑾峰彇灏忛儴浠剁被鍨 + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);// 鑾峰彇鎻愰啋鏃ユ湡 + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);// 鑾峰彇淇敼鏃ユ湡 + } + cursor.close(); // 鍏抽棴娓告爣 + } else { // 璁板綍閿欒鏃ュ織 + Log.e(TAG, "No note with id:" + mNoteId); + // 鎶涘嚭寮傚父 + throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + } + loadNoteData();// 鍔犺浇绗旇鏁版嵁 + } +/* + * 鍔犺浇绗旇鏁版嵁銆 + * 璇ユ柟娉曠敤浜庝粠鍐呭鎻愪緵鑰呬腑鍔犺浇绗旇鐨勫叿浣撴暟鎹紝骞跺皢鍏惰缃埌褰撳墠鐨 WorkingNote 瀵硅薄涓 + */ + private void loadNoteData() {// 鏌ヨ绗旇鏁版嵁 + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[] { + String.valueOf(mNoteId) + }, null); // 妫鏌ユ父鏍囨槸鍚︿负绌 + + if (cursor != null) { // 绉诲姩娓告爣鍒扮涓琛 + if (cursor.moveToFirst()) { + do { // 鑾峰彇鏁版嵁鐨 MIME 绫诲瀷 + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + if (DataConstants.NOTE.equals(type)) {// 鏍规嵁 MIME 绫诲瀷澶勭悊鏁版嵁 + mContent = cursor.getString(DATA_CONTENT_COLUMN); // 濡傛灉鏄枃鏈瑪璁帮紝鑾峰彇鍐呭鍜屾ā寮 + mMode = cursor.getInt(DATA_MODE_COLUMN); + mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + } else if (DataConstants.CALL_NOTE.equals(type)) { + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); // 濡傛灉鏄氳瘽绗旇锛岃幏鍙栭氳瘽鏁版嵁 ID + } else { + Log.d(TAG, "Wrong note type with type:" + type); // 璁板綍閿欒鏃ュ織 + } + } while (cursor.moveToNext());// 绉诲姩鍒颁笅涓琛 + } + cursor.close(); // 鍏抽棴娓告爣 + } else { + Log.e(TAG, "No data with id:" + mNoteId); // 璁板綍閿欒鏃ュ織 + // 鎶涘嚭寮傚父 + throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + } + } +/* + * 鍒涘缓涓涓柊鐨勭┖绗旇銆 + * 璇ユ柟娉曠敤浜庡垱寤轰竴涓柊鐨勭┖绗旇瀵硅薄锛屽苟璁剧疆鍏惰儗鏅鑹层佸皬閮ㄤ欢 ID 鍜屽皬閮ㄤ欢绫诲瀷銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param folderId 鏂囦欢澶 ID銆 + * @param widgetId 灏忛儴浠 ID銆 + * @param widgetType 灏忛儴浠剁被鍨嬨 + * @param defaultBgColorId 榛樿鑳屾櫙棰滆壊 ID銆 + * @return 鏂扮殑 WorkingNote 瀵硅薄銆 + */ + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId);// 鍒涘缓涓涓柊鐨勭┖绗旇瀵硅薄 + note.setBgColorId(defaultBgColorId); // 璁剧疆鑳屾櫙棰滆壊 ID + note.setWidgetId(widgetId);// 璁剧疆灏忛儴浠 ID + note.setWidgetType(widgetType); // 璁剧疆灏忛儴浠剁被鍨 + return note;// 杩斿洖鏂扮殑 WorkingNote 瀵硅薄 + } +/* + *鍔犺浇鐜版湁鐨勭瑪璁般 + * 璇ユ柟娉曠敤浜庡姞杞界幇鏈夌殑绗旇瀵硅薄銆 + * @param context 搴旂敤绋嬪簭涓婁笅鏂囥 + * @param id 绗旇鐨勫敮涓鏍囪瘑绗︺ + * @return 鍔犺浇鐨 WorkingNote 瀵硅薄銆 + */ + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); // 杩斿洖涓涓柊鐨 WorkingNote 瀵硅薄锛屼娇鐢ㄦ寚瀹氱殑涓婁笅鏂囧拰绗旇 ID + } +/* + * 淇濆瓨绗旇銆 + * 璇ユ柟娉曠敤浜庝繚瀛樺綋鍓嶇殑绗旇瀵硅薄銆傚鏋滅瑪璁板煎緱淇濆瓨锛屽垯灏嗗叾鍚屾鍒版暟鎹簱涓紝骞舵洿鏂板皬閮ㄤ欢鍐呭锛堝鏋滃瓨鍦ㄥ皬閮ㄤ欢锛夈 + * @return 濡傛灉淇濆瓨鎴愬姛锛屽垯杩斿洖 true锛涘惁鍒欒繑鍥 false銆 + */ + public synchronized boolean saveNote() { + if (isWorthSaving()) { // 妫鏌ョ瑪璁版槸鍚﹀煎緱淇濆瓨 + if (!existInDatabase()) { // 濡傛灉绗旇涓嶅瓨鍦ㄤ簬鏁版嵁搴撲腑 + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + // 鑾峰彇鏂扮殑绗旇 ID + Log.e(TAG, "Create new note fail with id:" + mNoteId);// 璁板綍閿欒鏃ュ織 + return false; + } + } + + mNote.syncNote(mContext, mNoteId);// 鍚屾绗旇鍒版暟鎹簱 + + /** + * Update widget content if there exist any widget of this note + * 濡傛灉瀛樺湪璇ョ瑪璁扮殑灏忛儴浠讹紝鏇存柊灏忛儴浠跺唴瀹广 + */ + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } +/* + * 妫鏌ョ瑪璁版槸鍚﹀瓨鍦ㄤ簬鏁版嵁搴撲腑銆 + * 璇ユ柟娉曠敤浜庢鏌ュ綋鍓嶇瑪璁板璞℃槸鍚﹀凡缁忓瓨鍦ㄤ簬鏁版嵁搴撲腑銆 + * @return 濡傛灉绗旇瀛樺湪浜庢暟鎹簱涓紝鍒欒繑鍥 true锛涘惁鍒欒繑鍥 false銆 + */ + public boolean existInDatabase() {// 濡傛灉绗旇 ID 澶т簬 0锛岃〃绀虹瑪璁板瓨鍦ㄤ簬鏁版嵁搴撲腑 + return mNoteId > 0; + } +/* + * 妫鏌ョ瑪璁版槸鍚﹀煎緱淇濆瓨銆 + * 璇ユ柟娉曠敤浜庢鏌ュ綋鍓嶇瑪璁板璞℃槸鍚﹀煎緱淇濆瓨銆傚鏋滅瑪璁板凡琚垹闄ゃ佸唴瀹逛负绌轰笖涓嶅瓨鍦ㄤ簬鏁版嵁搴撲腑锛屾垨鑰呯瑪璁板瓨鍦ㄤ簬鏁版嵁搴撲腑浣嗘病鏈夋湰鍦颁慨鏀癸紝鍒欎笉鍊煎緱淇濆瓨銆 + * @return 濡傛灉绗旇鍊煎緱淇濆瓨锛屽垯杩斿洖 true锛涘惁鍒欒繑鍥 false銆 + */ + private boolean isWorthSaving() { + // 濡傛灉绗旇宸茶鍒犻櫎锛屾垨鑰呭唴瀹逛负绌轰笖涓嶅瓨鍦ㄤ簬鏁版嵁搴撲腑锛屾垨鑰呯瑪璁板瓨鍦ㄤ簬鏁版嵁搴撲腑浣嗘病鏈夋湰鍦颁慨鏀癸紝鍒欎笉鍊煎緱淇濆瓨 + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())) { + return false; + } else { + return true; + } + } +/* + * 璁剧疆绗旇璁剧疆鐘舵佺洃鍚櫒銆 + * 璇ユ柟娉曠敤浜庤缃瑪璁拌缃姸鎬佺殑鐩戝惉鍣紝浠ヤ究鍦ㄧ瑪璁拌缃姸鎬佸彂鐢熷彉鍖栨椂閫氱煡鐩戝惉鍣ㄣ + * @param l 绗旇璁剧疆鐘舵佺洃鍚櫒銆 + */ + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; // 璁剧疆绗旇璁剧疆鐘舵佺洃鍚櫒 + } +/* + * 璁剧疆鎻愰啋鏃ユ湡銆 + * 璇ユ柟娉曠敤浜庤缃瑪璁扮殑鎻愰啋鏃ユ湡銆傚鏋滄柊鐨勬彁閱掓棩鏈熶笌褰撳墠鎻愰啋鏃ユ湡涓嶅悓锛屽垯鏇存柊鎻愰啋鏃ユ湡锛屽苟閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒銆 + * @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); // 閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒鎻愰啋鏃ユ湡宸叉洿鏀 + } + } +/* + * 鏍囪绗旇涓哄凡鍒犻櫎鎴栨湭鍒犻櫎銆 + * 璇ユ柟娉曠敤浜庢爣璁扮瑪璁颁负宸插垹闄ゆ垨鏈垹闄ゃ傚鏋滅瑪璁板瓨鍦ㄥ皬閮ㄤ欢锛屽苟涓旂瑪璁拌缃姸鎬佺洃鍚櫒涓嶄负绌猴紝鍒欓氱煡绗旇璁剧疆鐘舵佺洃鍚櫒灏忛儴浠跺凡鏇存敼銆 + * @param mark 鏄惁鏍囪涓哄凡鍒犻櫎銆 + */ + public void markDeleted(boolean mark) { + mIsDeleted = mark; // 鏍囪绗旇涓哄凡鍒犻櫎鎴栨湭鍒犻櫎 + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID // 濡傛灉绗旇瀛樺湪灏忛儴浠讹紝骞朵笖绗旇璁剧疆鐘舵佺洃鍚櫒涓嶄负绌 + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + // 閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒灏忛儴浠跺凡鏇存敼 + mNoteSettingStatusListener.onWidgetChanged(); + } + } +/* + * 璁剧疆鑳屾櫙棰滆壊 ID銆 + * 璇ユ柟娉曠敤浜庤缃瑪璁扮殑鑳屾櫙棰滆壊 ID銆傚鏋滄柊鐨勮儗鏅鑹 ID 涓庡綋鍓嶈儗鏅鑹 ID 涓嶅悓锛屽垯鏇存柊鑳屾櫙棰滆壊 ID锛屽苟閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒鑳屾櫙棰滆壊宸叉洿鏀广 + * @param id 鑳屾櫙棰滆壊 ID銆 + */ + public void setBgColorId(int id) { + if (id != mBgColorId) { // 濡傛灉鏂扮殑鑳屾櫙棰滆壊 ID 涓庡綋鍓嶈儗鏅鑹 ID 涓嶅悓 + mBgColorId = id;// 鏇存柊鑳屾櫙棰滆壊 ID + if (mNoteSettingStatusListener != null) { // 濡傛灉绗旇璁剧疆鐘舵佺洃鍚櫒涓嶄负绌 + mNoteSettingStatusListener.onBackgroundColorChanged(); // 閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒鑳屾櫙棰滆壊宸叉洿鏀 + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 璁剧疆绗旇鐨勮儗鏅鑹 ID + } + } +/* + * 璁剧疆妫鏌ュ垪琛ㄦā寮忋 + * 璇ユ柟娉曠敤浜庤缃瑪璁扮殑妫鏌ュ垪琛ㄦā寮忋傚鏋滄柊鐨勬ā寮忎笌褰撳墠妯″紡涓嶅悓锛屽垯鏇存柊妯″紡锛屽苟閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒妫鏌ュ垪琛ㄦā寮忓凡鏇存敼銆 + * @param mode 妫鏌ュ垪琛ㄦā寮忋 + */ + public void setCheckListMode(int mode) {// 濡傛灉鏂扮殑妯″紡涓庡綋鍓嶆ā寮忎笉鍚 + if (mMode != mode) { // 濡傛灉绗旇璁剧疆鐘舵佺洃鍚櫒涓嶄负绌 + if (mNoteSettingStatusListener != null) { // 閫氱煡绗旇璁剧疆鐘舵佺洃鍚櫒妫鏌ュ垪琛ㄦā寮忓凡鏇存敼 + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + } + mMode = mode;// 鏇存柊妯″紡 + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 璁剧疆绗旇鐨勬鏌ュ垪琛ㄦā寮 + } + } +/* + * 璁剧疆灏忛儴浠剁被鍨嬨 + * 璇ユ柟娉曠敤浜庤缃瑪璁扮殑灏忛儴浠剁被鍨嬨傚鏋滄柊鐨勭被鍨嬩笌褰撳墠绫诲瀷涓嶅悓锛屽垯鏇存柊灏忛儴浠剁被鍨嬶紝骞跺皢鍏朵繚瀛樺埌绗旇鏁版嵁涓 + * @param type 灏忛儴浠剁被鍨嬨 + */ + public void setWidgetType(int type) { // 濡傛灉鏂扮殑绫诲瀷涓庡綋鍓嶇被鍨嬩笉鍚 + if (type != mWidgetType) { // 鏇存柊灏忛儴浠剁被鍨 + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 璁剧疆绗旇鐨勫皬閮ㄤ欢绫诲瀷 + } + } +/* + * 璁剧疆灏忛儴浠 ID銆 + * 璇ユ柟娉曠敤浜庤缃瑪璁扮殑灏忛儴浠 ID銆傚鏋滄柊鐨 ID 涓庡綋鍓 ID 涓嶅悓锛屽垯鏇存柊灏忛儴浠 ID锛屽苟灏嗗叾淇濆瓨鍒扮瑪璁版暟鎹腑銆 + * @param id 灏忛儴浠 ID銆 + */ + public void setWidgetId(int id) { // 濡傛灉鏂扮殑 ID 涓庡綋鍓 ID 涓嶅悓 + if (id != mWidgetId) { // 鏇存柊灏忛儴浠 ID + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 璁剧疆绗旇鐨勫皬閮ㄤ欢 ID + } + } +/* + * 璁剧疆宸ヤ綔鏂囨湰銆 + * 璇ユ柟娉曠敤浜庤缃瑪璁扮殑宸ヤ綔鏂囨湰銆傚鏋滄柊鐨勬枃鏈笌褰撳墠鏂囨湰涓嶅悓锛屽垯鏇存柊宸ヤ綔鏂囨湰锛屽苟灏嗗叾淇濆瓨鍒扮瑪璁版暟鎹腑銆 + * @param text 宸ヤ綔鏂囨湰銆 + */ + public void setWorkingText(String text) { // 濡傛灉鏂扮殑鏂囨湰涓庡綋鍓嶆枃鏈笉鍚 + if (!TextUtils.equals(mContent, text)) { // 鏇存柊宸ヤ綔鏂囨湰 + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent);// 璁剧疆绗旇鐨勫伐浣滄枃鏈 + } + } +/* + * 灏嗙瑪璁拌浆鎹负閫氳瘽绗旇銆 + * 璇ユ柟娉曠敤浜庡皢绗旇杞崲涓洪氳瘽绗旇銆傚畠璁剧疆閫氳瘽鏃ユ湡銆佺數璇濆彿鐮佸拰鐖舵枃浠跺す ID銆 + * @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)); // 璁剧疆鐖舵枃浠跺す ID + } +/* + * 妫鏌ユ槸鍚︽湁鎻愰啋銆 + * 璇ユ柟娉曠敤浜庢鏌ョ瑪璁版槸鍚︽湁鎻愰啋銆傚鏋滄彁閱掓棩鏈熷ぇ浜 0锛屽垯琛ㄧず鏈夋彁閱掋 + * @return 濡傛灉鏈夋彁閱掞紝鍒欒繑鍥 true锛涘惁鍒欒繑鍥 false銆 + */ + 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銆 + * 璇ユ柟娉曠敤浜庤幏鍙栫瑪璁扮殑鍞竴鏍囪瘑绗︺ + * @return 绗旇 ID銆 + */ + public long getNoteId() { + return mNoteId; + } +/* + * 鑾峰彇鏂囦欢澶 ID銆 + * 璇ユ柟娉曠敤浜庤幏鍙栫瑪璁版墍灞炵殑鏂囦欢澶 ID銆 + * @return 鏂囦欢澶 ID銆 + */ + public long getFolderId() { + return mFolderId; + } +/* + * 鑾峰彇灏忛儴浠 ID銆 + * 璇ユ柟娉曠敤浜庤幏鍙栫瑪璁扮殑灏忛儴浠 ID銆 + * @return 灏忛儴浠 ID銆 + */ + public int getWidgetId() { + return mWidgetId; + } +/* + * 鑾峰彇灏忛儴浠剁被鍨嬨 + * 璇ユ柟娉曠敤浜庤幏鍙栫瑪璁扮殑灏忛儴浠剁被鍨嬨 + * @return 灏忛儴浠剁被鍨嬨 + */ + public int getWidgetType() { + return mWidgetType; + } +/* + * 绗旇璁剧疆鐘舵佺洃鍚櫒鎺ュ彛銆 + * 璇ユ帴鍙e畾涔変簡绗旇璁剧疆鐘舵佸彂鐢熷彉鍖栨椂鐨勫洖璋冩柟娉曘 + */ + public interface NoteSettingChangedListener { + /** + * Called when the background color of current note has just changed + * 褰撳綋鍓嶇瑪璁扮殑鑳屾櫙棰滆壊鍙戠敓鍙樺寲鏃惰皟鐢ㄣ + */ + void onBackgroundColorChanged(); + + /** + * Called when user set clock + * 褰撶敤鎴疯缃彁閱掓椂璋冪敤銆 + * @param date 鎻愰啋鏃ユ湡銆 + * @param set 鏄惁璁剧疆鎻愰啋 + */ + void onClockAlertChanged(long date, boolean set); + + /** + * Call when user create note from widget + * 褰撶敤鎴蜂粠灏忛儴浠跺垱寤虹瑪璁版椂璋冪敤銆 + */ + void onWidgetChanged(); + + /** + * Call when switch between check list mode and normal mode + * @param oldMode is previous mode before change + * @param newMode is new mode + * 褰撶敤鎴峰湪妫鏌ュ垪琛ㄦā寮忓拰鏅氭ā寮忎箣闂村垏鎹㈡椂璋冪敤銆 + * @param oldMode 涔嬪墠鐨勬ā寮忋 + * @param newMode 鏂扮殑妯″紡銆 + */ + void onCheckListModeChanged(int oldMode, int newMode); + } +} -- 2.34.1 From 209f82498cf623bf2ad500ee27c224fbf99625e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Wed, 25 Dec 2024 11:49:12 +0800 Subject: [PATCH 26/44] Signed-off-by: --- zhiliangfenxibaogao.docx | Bin 0 -> 19723 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 zhiliangfenxibaogao.docx diff --git a/zhiliangfenxibaogao.docx b/zhiliangfenxibaogao.docx new file mode 100644 index 0000000000000000000000000000000000000000..12ce26dc9bd1fc9f40e5ed729aab6732188202e0 GIT binary patch literal 19723 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
``rN zR!L4U3b>to6_WvAh8){Yw*ZzF8U}+RT;)%I=wHVuY<7oD`e?sk1&QdZhs2KwaO0p< zrG9x#VKj@Qv;A!Jh8jY;+shLS+~qU*rL+ZT2==AQ2f1Go^_?4#jfgNA9UbICmJ`7T z3qx)!Up;wP4sYORcN2W?wy>-6bE>X4ab70(*0os?01R~Kl8TmfZYp69nven#NfP5A z{Z!}91r5bbs7tLapb?`D#BqH9I%pw=IEO=@k);1!>`Nx7$}yaQ)}!}F_~gIik@I^H zSy&&d34$O10N4N!fPV&9{uPw`HO%s#043nZvcgB}|F^I5_;Kl8TKIr-(Ki2jm#utW zzG7(_h!PbDL=~3Bmym_16PTFB!CdpWF!H1B!Os>Z-0tn^r_tt%XzKImR9`~%b&DMo z7+Vl8vf5WTfgA{(@HFz|x_n?84sU<%p=gM84F8ncPjQ)&=9QY|vOBxNOmvJrlzon$ zRwWuvZrI8k<1q^KKpC{K-p7f_a;??E>pBb5&|UnoNwii-nU4KUYK>ynk=Ty|Q{5Xd zNlXI|l65xr9?*;X4bPDCQ9EP$eZ)V7lCGSe{y_V5>giG%JFgBTHXvZgT}0UPp$-8* zj5v`c!2D8d4$n+Hk1YP#nK?~P-LDA_@-p(7h44xt<#jT(3B(@FQRGbyJCOj0XYut0 z?Vp4znF`v&0Tcj$9|HgYacx+gfjcG0P=R$jIlj}$+zHdXz%Zu*?-GrK`G?WP z`88Z|RNwQD`AHWkYpB=R_Gj3pi!ja$M(-8EqUg{9j54#Gb1=VT1XALwtw37i!g9Bu zf5F2G_o;<;@qpXhNRW57s=B? zuqo5th06z1Yt<^2qw&mUZ{TcujCq^e^w+(WZ-Bh$rHQDTn%^oStv$o)d4*=STj!6g z=CIp#y&WJsXMVh6w*jJzKOuV29wf2V@d|yf39vb7aB;8n1}0A%C-Nw3nJH`hXhW1> zm`CJTKeci)q0A8sjnRG-*obw@XJG(EBZp$)f^wItSwAn|H2A;^C~3zR2R$qn6%0?l zQijXpetUdw8r>Hke| z`(^m+NoQyXwVNZ##LIrl&yQ_}HW>i^u(OCw!iV^RCN< z6?^eQAWq-6avRApyujQy3*aH7GqYb0sIOsRSRW9>6br6}1_|8L69}2JN$X_q5?1Bw z2Cz29x&s6*p$BCsxGR;|=M8QH$7H+nw8}$OXVd|;6S#>%<}=i;8X9MpoRL?%N@nKE z<`d$D9Jlz=IDrTT%GfLef(R~S7f8FS78(8t5G--W5m-Ndk3lIGhwp1lT+Bu+JFooOOomGXNAvvB|Qk?h&de*sq;%6Bgh0^ zAwF$>Z|)|6UA4##7}mJ+WEaySOg7vRI0H0bt{oI!1({wMO*IUSd{vE@MFn+?7o&mP z%Yt83aq_V=YCeZQNq9c_?x`^x*?>`tjAfcCt$5 z{%x5ZS=XMN$6c4zNCwx$$+GlGeo zB0*ws$Q^RYalw%OStm-_iGZ*Dw7uHvU;UzHJW)~d>X%Ml6R=L zY2+R53(?Zi)-V9Ao|y^Db*V$8;6mptFQ5^PjV$!0?y8%LSE~&5xC@o!_ThZrf^y|( z&`VyykVRUg17FKqEUIow4O>RnE-0%ink$Q@VIeJAn+eOUQg{Q)nv!wg! zBj@_xbLZ3rQR{q-t>H3@85$R5t@(#Ix@EcUN{rJey!nrbf3O~dVj1fKYc zLZzgV&wWD-Wy>X0a$lte9v8Q+k4)0u<`A#c^1P=v;Xc9N-h>af?-yNEatK5^KVj=` z=^Y5W;X};KCSc!dMoU{YB{IPQQ!b*bq1Vmz5x*mwgD=IkhQ`jYik z^RogOw33HTJAtE#4$Nd?SBmw=YUDqd0H7|9YCk?^>23I5)>s^j939Q9O&tE1q3cvN z9F|y-z1CA-;h-laE>?rcPI)67QhzvqG9z|FYJe~&93>dlto{D(8BK{5-*-ruc`5{Ae-2gL;HhyTP;e zEaGtnR#0Kf~L}JEHG`F&XU4+zFeZ-C-4%xMqFd#giI~*#W4qEqJ_aW z$aFb7^~pO4Fc9(55k!o{zN`jvKfuaUQ=;*0( zIJmXh?>PdhE^iAyT@9Q%OV+O7y~#H#;?~z1SGko93!fSN!wHAYB=eNE7e8p)KgkGv zwF4+=gYu>(#(U3R8=^a$k!=vdw5kVTzt#__jUP1eQ(cBcmai|2UpTvR0Vjg#rvgV} z(W}p&#PkLCF~WQmzo%8I)WmV-nR-uvy6uz*GFloI{V=<)7pzdfo;TOoZeVn269($v za58#BmV}VDp#G@>n(#W0A891#+$dgY*>sfP|0SxasbkuX54nIY%bz-o#2r$rLiK0{ zMr=p@xj5H3a0*OD_9aF^w7ez_oJjRYAa8y|Fmb}lj24)JK?gC#BaFm>9Z^6Wt`K#V zXxJoOv(9L^V-7vv@vt3ZJHNa35JB?(*O4wzg}&;JO+EWDS0DobK~Oh$YYN|^;V6Ua z75LzTUV@G3?&cK*2zB!P9fata9YkwB-n^)bD)$QpaNAN!Z{IET69#NO5V?TEXZMIi z8&tWtafsQVCQ+o~%5~$2a%EK?KtIAPzD0!YMxzX;?lGs#R7!`pjPMK$hP@MTSVRu6w-7gIK2^sm;grT+Op!00F$ zB1$i~ge0-jmMKCE;Bu7-?aTRo1gRD>phK%8q#*4zp+I8KSC-zA>C2SYESe8SdCZr$ z>?!m)aW31`Z-W&YsdzrJZArFQ!(3|4XEg}VVm)t*SvYzOI>=}ky$pd3F2{5+y;R>8 z0xw?_{=9JuYAVEKvl>D_GiX>d*;@9$L(gEJTd)w~@4c;1Ugw_-&7D($0CsJiI|wi^ zfN$8-U~MRLZq1y(`ij|9c-ZKA3Q~B!pe&)>>>q05o0A-JE@ z6rYClxsXU2K?BN zWhv(j2I76>T9`ncyxw9n=YhNhGO9^6ybou(&&9bO~{9%RXxhftzs?k2#>!8^X8l88=5EW%~_Mf+ioIOl(1M^guaAz z*2{p0+ef6niJwH1=fDQM}4V9Fud+rNNwj z5bB%}hdp2mCQ-Iy4wST@LuAZuZmh*?*DZ#ot*h3d0fRTg)m0hvUIWa58KG6h;tRXx zAW3onibQdM;meZ+CPzyL_SUInHTRzR@PPYag8#P%IQTFE(D+e<_u<{3ZxjOfAT+z9Oec8LF{b9WY+J^=zI5HS-Q^99^UP_d>%5=Q1E@s&N~Hc`m7?^Yj-~L!&`|lv*FgLj$3LAu zYbPsxBl{17;tzYGJZaT>nHhQT9Q@=n2w>}i5Px{Bp<1y*-64HGX^@^2u1~CK_$MBa zxZ2FtJGEzg0wOtQN}3qt^%Q!yiRgEMEP<=lXRf?8R0$yvoyYFmv3uG{LFotMU2pfM zR&fs*vpxqw&_RlK42t*H@v6?Y<$^Z-XjM#sN!liH7o&!&PZguc0Es9mcuwP@#DRzw zo|%wfeP5;Mp`H2n04Ds@UX{dS^$MW(Q7@_Fy~0QDdQF=JIkqi*9b!;K&D(fr{ib3d zDEX$ryCJ_I;XywY+cJ`aDJ#J-egmN|{hDuzc_?Xn$%uFok2)i&_bow^;C4Fn7D+Q9 zCB3^o2HNae#Tr21s)W1>x=ApN6H)x;yWWI;Fkb+Vl&oPJJJ6@=sQ^%2{6WUC&qC!d z;#YOIokuSfH+7db=@Y;+dNXN!7f(lYbxU5?r|wD?gMe*AUYyU-tr9w=p^br^XYkph@es8q4ijkO zyMseXsK%$Zz<}X!!F{-RGd6vb0}TkNfDMC>8nEfB%dv|VQAY8C17X0je2es>0Nj|M z00oMAnde?Z3hXn{wr#DWeQt#?QAC}D3_@KsedL1OjYW>Ntk;wky~X7c!7<&iPTy?X z&Q2suK+32lwt)rOrN-ug68C0})V_9Z^5=?}Bd5w=OV+#I!Y=5HL}Sk4Sz>3v4@xoh zFC8Vg-PeZ3R(vLu%X`wc73D;m15|?+hWOS}7Fe<}?wESCwEX1Gs$ShoC%umw8|MZB zpLFdJu{;o#-MVcfHoQRuUfR|!o+7lxnENUG(7r?Z_4@ibWFQ1B-Eb;#@D$zs{2AD7 zwg-){oNxd-jfa^DOPeMGkF7=edF%2JAo&uc`;ur{kB6U0dh`lL3$cQ3fty{(Il$xk~XQYBtLXJ;}c~VPFOCl1~QkSMs`s{V{WkZdphy* z3mOrbdC(OS(^q7c@VjcPXyJ2$xb=ISN5-ei#eJJNR#&O-8J9P@v~BHN5T=+{Aq8mQ z{%HGfHm7iYZC}g;^|Pk^yP3wpzpX#N-E+6Lq@&H#jROOUUbl+5(x#yGZ=KC4<4l7D9m@}S zz1i?Y5jyXqg{=z?K#b?#9lr%1XRU~#HX>ZpYfPHqRnA-fopZ-57F@+e42RyCI*lwUk=AZOB-m^M zYzs^LYKzvS@8!yOjkxpM=w~dd-Zolh+?o)Wh8;eGIPH)so%jb`-Wd&=iLqX6=(P!( z&)!iusny+i2M!b zR4M`84u{JXX*zI>c569d$c`-*tY*c1cn(};kEcRcfXi6w&OzKT4JL>Mq4bfoPntmA zEwAi|m-OsFV=Jma$hvrj6YLeP?zse(@lg?c>G`9)D}u|X*j)2cMhZqLJ@@tk91%x_ zF~MP@XyC9Wsq#Be=J@TY;scJ?u(GEaTpBPAZWTgEcMGJWVNP(@y1H}BZzZ-jqtcv? zuW|x6*|Cs#u$=9^oDHmIOsqDge{aNcai|?|7!6dsPz_L1zlo~F*cTf4go%73<&7x@ zDxs9*3=GtlfftcZ{R4MS}V*ewmsu?Azgep!UG1$M$y(v%{u3bgGi z+F;5kiTXL(IC77oR+&?#rhdyUtP-Wl+k*?m!myd$y^dV-DR9_col)4*WuA-RvQ~5a z4vtCw=DykqfYVUzj0!rf28Z)Ou+pd?gL`_dCGi!j!T1)gEM0Bb;+k<z)M7InrsJ`Fq~~Tl{~>bri^l2|#N+DO}2PT0M{SFT;c@RuLb4C>lB3bHbN3~`hrLczfc zh;EnSJGgLVc>T|i!zr9(I~2te?OhQLL67!NVL`77tB`PQ8K_?yC})R7>SvYa-fUl! zA(=GaiTvahB;lsLxf~mUj@K74Csa(L4dV94o-|4rCRJQ0vd-V`Xx^22MwUXugS85GU`dSH z{Z@z$d^oxfo!=QsOl4ZTfiYakg@@aBysz#ZRceNoqjx(qCzl^4ZD-5`-c z6RR^_(pV^#H0kWdN+7H-{8eTdtGeFf6%>Jk<(WL$G9)wf_i@I{Z$GGJ+VERqUNnHw80fGjxlECIGV!WJ-fo)NrVcVi> z`$)0cnR8vhV1{LbW>vODlKW?>O}cvybw77*VyhbaZ-&W{QPuN%JY`RlTORIK+I95S zoR;YX6_n|kwpHWd<~fMiK{+o(fTc53#wT~HeuLK0q4*r2NYc*Y`%Se3OF$=Vp5r^^ z=Xjs?2I^7U?Q|%cgJ)i&`%5Xt%{GFSC4}!b&oUBELM9G}qYg5PPC}$1N6n)~G(X_T z#v+arM$RDYCEcOG$+Ov9)FpiG$@ySp)M6Eb`rT0nZRXVkKYXDO4tP&%va1PXeWAuO zfl%cC^{YS#$Lt;_OlsL2u8-0#k>9AR;9Kp5n%$Bm>o3$V4WbPh0m&q;`@9$M?5S!0_R;nMFIP&7?Mc7Z~+l~w; zP_l9KuQnTNVmGsTG+Ns!EhCrh%;e^qUvU3wU%fhr!G5I?{fr+Se}m1`zA*pL7nk^L{q@`^MGbV9Vnv^~u&&%|g zo(b#zWFeA+(vY{3AEXzQxWpxU3Q|5Bvq8wz)g%EUHz6;OkUmRGr(Y2C@$0kUMQPhO zOx=}&AX1YOyb&pL)`mzsHqdAxe2P>EQi`~vMDRS&elN`NK$R@`^J16eAJxu~CUe75CD*~*D~P3FVM zlfC~YcZu*wYi;q(Ye zk*2ZIoTewv^7e_V#v5-ylACX}$(M}$+m|w5^T9zs!P(YwLjs`JH|kd?il54_5*1JG zaveG+VL~tBYk*moP4rXLPvqsyl5TYKQnSWSr-sRl@tMjTW@sbpmCOeSBkMS$^IsE} zXSD~ydlW}mZk&F$;vK9$fodGdD&P-XFP4hsucVEDv(K@JftNGGIC;+l%6aAqtYJ4$ znoOBzG{D^t!Uf%rw%-hC%D1p0fGaNVfNz2_(tzi-m8hi1?DkFyvWOVmIttKJm=ae( z%YNI*D-aYtD*?wEf}2df0(CG{ySNf<^LUw!pLgo_VV|*=j2g{LS6AZEtLj&`Q43eH zc0jQhBu~0an9x@XEE$X)@0SZZk(3m=DUy%9l%?`$C6f)8NvP)4koMQ6b!@=)?=J5h3n+-U8=OPzae*8E#Ze;G=(l8xkJ%__!7W5z*Q zsigqSjKS?mUshg(6fnph;OkvZ5~1$IH$j=tcf>5Ney|*yvE~H?Zxsyo+QExewm}6o!D|=}U!`24V zLeLG=g|hyuF%D=Va;X=k2eQzQX&|q7T0}@nIe4^)oRy;3zqi{N}hrO*;>)x-U6Hv#6bIO_HqrnbIKR8))RJt9q;FTK6 z(Gbty@4}z>xAxXD*>%zvme@PjmmW7>b-bU)b>3Du!IZ=-Vf`u}-oe?mJGd849tNx4 z-o|mgqRwhNSZ8q-VQ@AFdpYi%R%YklUrz21I*t@|&yQUYySHBr=n#K;|HxT?6==G0 z<%wCZe4j^MfJsdjtP=c`H9Dq3?K@UyJnnKFXxlvZxdN?&WxhpH&g3c*(H@T~J!N$N zfWrT$@ziqWUhR}k^)SLDmO*u;ky}HTn|3?!th`Cr9Gs^a0wh!NoD6^@>^*EWP7=ql zoFn$z+Z0>CZGW(fj@QffxgQ=gw^M;$#qo&=U&pQj+)tgy_V#$8p@jy&c+PJZNaV<`_LUXdq?cN9TLup3E0{`a3hRZk z;VA-E4*j%RV8`>uvuHNAoM%yM17SFotj|0y1g4!=y z$Vd{l$?F1EhD{vMvJrDvK2O^XgKx=zn5a_E)>JAVfl;@89$~6yZLv+tCxUi%e$*Ied~ai)gU)zoVgMM^ zL#Fd?xrt3e##J@UEg7^+_9j$N7E+=p4aSeavbEclrao<)>0e$mkZlbj;^xD^^(+6_ zF?ml<BddnphP{burEX2mFZAR3Z&x>bE+(27Ur)(R5ydlYeQf;7v($h+H z{zgEMraJ>{caQz=-cju?v}i`Q{dJq-g``UZq`GmX5u;*Z+L{rKh{)&?hQi`RYGJ5j z+5WGC(Rapqnqn3}L&?U3reRPPWpe14>a=`g4W%go-I`{6l$=Lmq#<5R6%ECRC^au} z$t%=7=-YVav`^f~iTfF~NJ2$^V#n74*_yma(J5mj?rx{Bx zv|xt0g@d!fU5EbF6S*a{?#mJ_cp1qSdj?YXs1VlAtYdI;YqP0=s*pWt0WQM_5U|2M z#G(Ua1MF{6gT+yTWb8ph()6t6_%-gy@2AYMYxI6v0birMBg|oMGj_3EO;kB*_J+n= z=UH&M-3dg^Ccn&8J_HO8OLQQA-;)QS*0UusuZpB5>Xo97Em6vpWOroW%0r3&I(+H6 z@9Oc8CpL9~Mtqy&#CQmwq_ieSDWWT|jwa>lM25<+J@6vXe|5UOO!KXh1!a^CO$Pm4 zt>lE?U9U@3W^vC1P`GTZ2D7IxIYQGr0_3pb_ z_z@C`>i|h$yMfH>q=>`}-Shg#tnlCS1!s6Gx5Y^6WEWQwteW@g6cJ0}EoKseN(F%C~3dg}WR&!;`=wMoQX6 z1TzK2gvq=99)kuriLz4C24E!rLs=+}eWM>>Jc0};g6W&D?otVMEh0%X;+SGV+LqrX z8Z)S**8Ct4v_y(t6WEfFgrJ6DkA~r8X)RccEi?v1jIhBLeA_*yEdMu4(*)om(eYBB z__@r5mSlFTNFz0NqoNw+M@TQs^);5frIK=&en26;9^gu!R867T>tdN#>-Elz$Bs2m7;jLeKB73YHa zb1Y+X^>Iz#uj&Pm7V$@+g3b}70)LK01{`)ZeGPvnswh9X4HR&*?;FhNn=nI&<+(}6 zT!7#;%llDq6tn*b3NwJ3CFt*q#EfiP*OANJ3dEdHZ54<{FYm$GNr~D;44}zTl!zP~ zw|g)C70dE-{Gd#;m<)jBSCF9|oZT{&%RUC(8Pu}`JkUqP1=(0;8cZ5ZtcO}OR_E$u z{_UidYxM)4E-!$Er3pv@56kk9b0W^x>9Z%s4rMQYq;wIK<#wbvLdx3Q6wIs$T^Um4 z9o<=4AX|yfzT-EbB_mT?0*#9|lLvPW&HeKynDK3H8jF*#C)gdv)a*4MV%bDdT z{+mmqDHTrJft_oxNmi;@Qq{H(tmdJ17_vKce@M7+L;FI(gP8_9g7DI8ZUs1*Ygn}A zwvn@EA}1-Eozd6ISRshat#-N@Kls&?M^%gYfga*aMFT6#5@$hYt@R6j=IfzAJYF;2 zG&L+X_8Asf?<-XBYZXI^^!UumhQZ;h+MQ2cf~%%a%>6cj-_QbOJi(dm&Rl0TIOxI! zf`t=$Ft6yft=8z$q{n??i$z+WR=3;x8ypPg9C+$bR8T!>!}Y#S2^;Zx+H%^S!f4U> z(TWZcNe(S;eq%ndbVyg>830d6q=ZZDgnUWw(h!2LjuVClq~IU&H-Ym)!cW#Q{%0|B zg*HnquEksU!O zk|1;B5QlBS6cLj&7*#blTkqH_q;NbS4Sj!>Yi$n{(xUrbWSU)KE{pydhFcoU>-p6j zIIMr5n;Lnl>oZ2f>b{73U%xoy6rtB-%CI@q5o=X#f3ncokP@pp!DpXPRiy?VrX`>rI-3Dmy zdTB`*WQ)b^jQ8sQq$2_XQGIxJ|9ryHFOT}?$A38QNLJ$S1b;tdzxsFl z-(8u%&;S7VAM5o0<=FfV|GS;}E4+{I7x=%e&fn30f9?N@b`bmp{jX{I4Y5P% literal 0 HcmV?d00001 -- 2.34.1 From a0f7a501cf8979c3d216096d6983ae0e147e47b0 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:17:29 +0800 Subject: [PATCH 27/44] Delete 'ActionFailureException.java' --- ActionFailureException.java | 45 ------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 ActionFailureException.java diff --git a/ActionFailureException.java b/ActionFailureException.java deleted file mode 100644 index 74811eb..0000000 --- a/ActionFailureException.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* - * - */ -package net.micode.notes.gtask.exception; -/* - * 自定义异常类,表示操作失败。 - * 继承自RuntimeException,用于在操作失败时抛出异常。 - */ -public class ActionFailureException extends RuntimeException { - private static final long serialVersionUID = 4425249765923293627L; - - public ActionFailureException() { //默认构造函数。 - super(); - } -/* - * 带错误信息的构造函数。 - * @param paramString 错误信息 - */ - public ActionFailureException(String paramString) { - super(paramString); - } -/* - * 带错误信息和原因的构造函数。 - * @param paramString 错误信息 - * @param paramThrowable 原因 - */ - public ActionFailureException(String paramString, Throwable paramThrowable) { - super(paramString, paramThrowable); - } -} -- 2.34.1 From ed746a70ecb947104c324a85fa26aabf6eb2e537 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:17:42 +0800 Subject: [PATCH 28/44] Delete 'Contact.java' --- Contact.java | 81 ---------------------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 Contact.java diff --git a/Contact.java b/Contact.java deleted file mode 100644 index f0fccea..0000000 --- a/Contact.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.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 { //缓存联系人信息的HashMap,键为电话号码,值为联系人姓名 - private static HashMap sContactCache; //日志标签 - private static final String TAG = "Contact"; //查询联系人信息的SQL选择语句模板 - - 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); - } - //替换SQL选择语句中的占位符 - 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; - } - } -} -- 2.34.1 From b492066dc6806533682dfc78a9d7d6a577765161 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:17:49 +0800 Subject: [PATCH 29/44] Delete 'GTaskASyncTask.java' --- GTaskASyncTask.java | 170 -------------------------------------------- 1 file changed, 170 deletions(-) delete mode 100644 GTaskASyncTask.java diff --git a/GTaskASyncTask.java b/GTaskASyncTask.java deleted file mode 100644 index ed52f7b..0000000 --- a/GTaskASyncTask.java +++ /dev/null @@ -1,170 +0,0 @@ - -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.remote; - -import android.app.Notification; -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 { - - private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; - - public interface OnCompleteListener { //定义一个接口,用于在任务完成时通知监听器。 - void onComplete(); - } - - private Context mContext; - - private NotificationManager mNotifiManager; - - private GTaskManager mTaskManager; - - private OnCompleteListener mOnCompleteListener; -/* - * 构造函数,初始化上下文、通知管理器、任务管理器和完成监听器。 - * @param context 上下文 - * @param listener 完成监听器 - */ - public GTaskASyncTask(Context context, OnCompleteListener listener) { - // 初始化上下文 - mContext = context; - // 初始化完成监听器 - mOnCompleteListener = listener; - // 获取通知管理器 - mNotifiManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); - // 获取任务管理器实例 - mTaskManager = GTaskManager.getInstance(); - } -/* - * 取消同步操作。 - */ - public void cancelSync() { - mTaskManager.cancelSync(); - } -/* - * 发布进度消息。 - */ - public void publishProgess(String message) { - publishProgress(new String[] { - message - }); - } -/* - * 显示通知。 - * @param tickerId 通知的提示文本ID - * @param content 通知的内容 - */ - private void showNotification(int tickerId, String content) { - // 创建一个新的Notification对象 - Notification notification = new Notification(R.drawable.notification, mContext - .getString(tickerId), System.currentTimeMillis()); - // 设置通知的默认灯光效果 - notification.defaults = Notification.DEFAULT_LIGHTS; - // 设置通知的标志,自动取消通知 - notification.flags = Notification.FLAG_AUTO_CANCEL; - // 创建PendingIntent,用于在用户点击通知时启动相应的Activity - PendingIntent pendingIntent; - if (tickerId != R.string.ticker_success) { - // 如果通知不是成功通知,启动NotesPreferenceActivity - pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesPreferenceActivity.class), 0); - - } else { - // 如果通知是成功通知,启动NotesListActivity - pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesListActivity.class), 0); - } - // 设置通知的详细信息 - notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, - pendingIntent); - // 发送通知 - mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); - } -/* - * 在后台线程中执行同步操作。 - * @param unused 未使用的参数 - * @return 同步结果状态码 - */ - @Override - protected Integer doInBackground(Void... unused) { - // 发布登录进度消息 - publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity - .getSyncAccountName(mContext))); - // 调用任务管理器的同步方法,并返回同步结果状态码 - return mTaskManager.sync(mContext, this); - } -/* - * 在UI线程中更新进度。 - * @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]); - } - } -/* - * 在UI线程中处理同步结果。 - * @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(); - } - } -} -- 2.34.1 From 8d07e4ae6dab6f44762741cbf5b7643e44e7b105 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:04 +0800 Subject: [PATCH 30/44] Delete 'GTaskClient.java' --- GTaskClient.java | 807 ----------------------------------------------- 1 file changed, 807 deletions(-) delete mode 100644 GTaskClient.java diff --git a/GTaskClient.java b/GTaskClient.java deleted file mode 100644 index d2d0669..0000000 --- a/GTaskClient.java +++ /dev/null @@ -1,807 +0,0 @@ -/* - * 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(); - - private static final String GTASK_URL = "https://mail.google.com/tasks/"; - - private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; - - private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; - - private static GTaskClient mInstance = null; - - private DefaultHttpClient mHttpClient; - - private String mGetUrl; - - private String mPostUrl; - - private long mClientVersion; - - private boolean mLoggedin; - - private long mLastLoginTime; - - private int mActionId; - - private Account mAccount; - - private JSONArray mUpdateArray; -/* - * 私有构造函数,初始化成员变量。 - */ - private GTaskClient() { - mHttpClient = null; // 初始化HttpClient为null - mGetUrl = GTASK_GET_URL; // 初始化GET和POST请求的URL - mPostUrl = GTASK_POST_URL; - mClientVersion = -1; // 初始化客户端版本号为-1 - mLoggedin = false; // 初始化登录状态为false - mLastLoginTime = 0; // 初始化最后一次登录时间为0 - mActionId = 1; // 初始化操作ID为1 - mAccount = null; // 初始化账户为null - mUpdateArray = null; // 初始化更新数组为null - } -/* - * 获取GTaskClient的单例实例。 - * @return GTaskClient的单例实例 - */ - public static synchronized GTaskClient getInstance() { - // 如果mInstance为null,创建一个新的GTaskClient实例 - if (mInstance == null) { - mInstance = new GTaskClient(); - } - // 返回GTaskClient的单例实例 - return mInstance; - } -/* - * 登录Google任务服务。 - * @param activity 当前活动 - * @return 是否登录成功 - */ - public boolean login(Activity activity) { - // we suppose that the cookie would expire after 5 minutes - // 假设cookie在5分钟后过期,需要重新登录 - // then we need to re-login - final long interval = 1000 * 60 * 5; - if (mLastLoginTime + interval < System.currentTimeMillis()) { - mLoggedin = false; - } - - // need to re-login after account switch - // 需要重新登录以切换账户 - if (mLoggedin - && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity - .getSyncAccountName(activity))) { - mLoggedin = false; - } - // 如果已经登录,直接返回true - 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; - } - - // login with custom domain if necessary - // 如果账户不是gmail.com或googlemail.com,尝试使用自定义域名登录 - if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() - .endsWith("googlemail.com"))) { - StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); - int index = mAccount.name.indexOf('@') + 1; - String suffix = mAccount.name.substring(index); - url.append(suffix + "/"); - mGetUrl = url.toString() + "ig"; - mPostUrl = url.toString() + "r/ig"; - - if (tryToLoginGtask(activity, authToken)) { - mLoggedin = true; - } - } - - // try to login with google official url - // 尝试使用Google官方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; - // 获取AccountManager实例 - AccountManager accountManager = AccountManager.get(activity); - // 获取所有Google账户 - Account[] accounts = accountManager.getAccountsByType("com.google"); - // 如果没有可用的Google账户,记录错误日志并返回null - if (accounts.length == 0) { - Log.e(TAG, "there is no available google account"); - return null; - } - // 获取设置中的同步账户名称 - String accountName = NotesPreferenceActivity.getSyncAccountName(activity); - Account account = null; - // 遍历所有Google账户,查找与设置中同步账户名称匹配的账户 - for (Account a : accounts) { - if (a.name.equals(accountName)) { - account = a; - break; - } - } - // 如果找到匹配的账户,将其赋值给mAccount - if (account != null) { - mAccount = account; - } else { - // 如果没有找到匹配的账户,记录错误日志并返回null - Log.e(TAG, "unable to get an account with the same name in the settings"); - return null; - } - - // get the token now - // 获取授权令牌 - AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, - "goanna_mobile", null, activity, null, null); - try { - // 获取授权令牌的Bundle - Bundle authTokenBundle = accountManagerFuture.getResult(); - // 从Bundle中获取授权令牌 - authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); - // 如果需要使授权令牌失效,使令牌失效并重新获取 - if (invalidateToken) { - accountManager.invalidateAuthToken("com.google", authToken); - loginGoogleAccount(activity, false); - } - } catch (Exception e) { - // 如果获取授权令牌失败,记录错误日志并返回null - Log.e(TAG, "get auth token failed"); - authToken = null; - } - - return authToken; // 返回授权令牌 - } -/* - * 尝试登录Google任务服务。 - * @param activity 当前活动 - * @param authToken 授权令牌 - * @return 是否登录成功 - */ - private boolean tryToLoginGtask(Activity activity, String authToken) { - // 尝试使用当前授权令牌登录Google任务服务 - if (!loginGtask(authToken)) { - // maybe the auth token is out of date, now let's invalidate the - // token and try again - // 如果登录失败,可能是授权令牌已过期,使令牌失效并重新获取 - authToken = loginGoogleAccount(activity, true); - if (authToken == null) { - Log.e(TAG, "login google account failed"); - return false; - } - // 使用新的授权令牌再次尝试登录Google任务服务 - if (!loginGtask(authToken)) { - Log.e(TAG, "login gtask failed"); - return false; - } - } - return true; - } -/* - * 使用授权令牌登录Google任务服务。 - * @param authToken 授权令牌 - * @return 是否登录成功 - */ - private boolean loginGtask(String authToken) { - // 设置连接超时和套接字超时 - int timeoutConnection = 10000; - int timeoutSocket = 15000; - HttpParams httpParameters = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); - HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); - // 创建DefaultHttpClient实例 - mHttpClient = new DefaultHttpClient(httpParameters); - // 创建BasicCookieStore实例并设置到HttpClient中 - BasicCookieStore localBasicCookieStore = new BasicCookieStore(); - mHttpClient.setCookieStore(localBasicCookieStore); - // 设置HttpClient不使用Expect-Continue - HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); - - // login gtask - // 登录Google任务服务 - try { - // 构建登录URL - String loginUrl = mGetUrl + "?auth=" + authToken; - HttpGet httpGet = new HttpGet(loginUrl); - HttpResponse response = null; - response = mHttpClient.execute(httpGet); - - // get the cookie now - // 获取cookie - List cookies = mHttpClient.getCookieStore().getCookies(); - boolean hasAuthCookie = false; - for (Cookie cookie : cookies) { - if (cookie.getName().contains("GTL")) { - hasAuthCookie = true; - } - } - if (!hasAuthCookie) { - Log.w(TAG, "it seems that there is no auth cookie"); - } - - // get the client version - // 获取客户端版本 - 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) { - // simply catch all exceptions - // 捕获所有异常 - Log.e(TAG, "httpget gtask_url failed"); - return false; - } - - return true; - } -/* - * 获取操作ID并递增。 - * @return 当前操作ID - */ - private int getActionId() { - // 返回当前操作ID并递增 - return mActionId++; - } -/* - * 创建HttpPost请求。 - * @return 创建的HttpPost请求 - */ - private HttpPost createHttpPost() { - // 创建HttpPost请求,设置请求URL - HttpPost httpPost = new HttpPost(mPostUrl); - // 设置请求头,指定Content-Type为application/x-www-form-urlencoded,字符集为UTF-8 - httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); - httpPost.setHeader("AT", "1"); - // 设置请求头,指定AT为1 - return httpPost; - // 返回创建的HttpPost请求 - } -/* - * 从HttpEntity中获取响应内容并返回字符串形式。 - * 支持处理gzip和deflate压缩编码。 - * @param entity HTTP响应实体 - * @return 响应内容的字符串形式 - * @throws IOException 如果读取输入流时发生错误 - */ - private String getResponseContent(HttpEntity entity) throws IOException { - String contentEncoding = null; - // 检查HttpEntity是否包含Content-Encoding头部 - if (entity.getContentEncoding() != null) { - // 获取Content-Encoding的值 - contentEncoding = entity.getContentEncoding().getValue(); - // 记录日志,显示当前的Content-Encoding - Log.d(TAG, "encoding: " + contentEncoding); - } - // 获取HttpEntity的原始输入流 - InputStream input = entity.getContent(); - // 如果Content-Encoding是gzip,则使用GZIPInputStream解压缩 - if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { - input = new GZIPInputStream(entity.getContent()); - } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { - // 如果Content-Encoding是deflate,则使用InflaterInputStream解压缩 - Inflater inflater = new Inflater(true); - input = new InflaterInputStream(entity.getContent(), inflater); - } - - try { - // 创建InputStreamReader,用于将字节流转换为字符流 - InputStreamReader isr = new InputStreamReader(input); - // 创建BufferedReader,用于逐行读取字符流 - BufferedReader br = new BufferedReader(isr); - // 创建StringBuilder,用于存储读取到的内容 - StringBuilder sb = new StringBuilder(); - // 循环读取输入流中的每一行 - while (true) { - String buff = br.readLine(); - // 如果读取到null,表示已经到达流的末尾,返回StringBuilder中的内容 - if (buff == null) { - return sb.toString(); - } - // 将读取到的行追加到StringBuilder中 - sb = sb.append(buff); - } - } finally { - // 确保输入流在方法结束时被关闭 - input.close(); - } - } -/* - * 发送POST请求并返回响应的JSONObject。 - * @param js 要发送的请求数据,以JSONObject形式表示 - * @return 服务器响应的JSONObject - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如未登录或响应内容无法转换为JSONObject) - */ - private JSONObject postRequest(JSONObject js) throws NetworkFailureException { - // 检查用户是否已登录,如果未登录则抛出异常 - if (!mLoggedin) { - Log.e(TAG, "please login first"); - throw new ActionFailureException("not logged in"); - } - // 创建HttpPost对象 - HttpPost httpPost = createHttpPost(); - try { - // 创建一个LinkedList来存储请求参数 - LinkedList list = new LinkedList(); - // 将请求数据(JSONObject)转换为字符串,并添加到请求参数列表中 - list.add(new BasicNameValuePair("r", js.toString())); - // 创建UrlEncodedFormEntity对象,用于将请求参数编码为表单格式 - UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); - // 将请求实体设置到HttpPost对象中 - httpPost.setEntity(entity); - // execute the post - // 执行POST请求 - HttpResponse response = mHttpClient.execute(httpPost); - // 获取响应内容并转换为字符串 - String jsString = getResponseContent(response.getEntity()); - // 将响应内容字符串转换为JSONObject并返回 - return new JSONObject(jsString); - - } catch (ClientProtocolException e) { - // 捕获并记录HTTP协议异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("postRequest failed"); - } catch (IOException e) { - // 捕获并记录IO异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("postRequest failed"); - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - 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 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如JSON处理失败) - */ - public void createTask(Task task) throws NetworkFailureException { - // 提交更新操作 - commitUpdate(); - try { - // 创建一个新的JSONObject,用于存储POST请求的数据 - JSONObject jsPost = new JSONObject(); - // 创建一个JSONArray,用于存储任务的创建操作 - JSONArray actionList = new JSONArray(); - - // action_list - // 将任务的创建操作添加到actionList中 - actionList.put(task.getCreateAction(getActionId())); - // 将actionList添加到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - // 添加客户端版本信息到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - // post - // 发送POST请求,并将响应内容转换为JSONObject - JSONObject jsResponse = postRequest(jsPost); - // 从响应中获取结果数组,并获取第一个结果 - JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( - GTaskStringUtils.GTASK_JSON_RESULTS).get(0); - // 从结果中获取新创建任务的ID,并设置到任务对象中 - task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); - - } catch (JSONException e) {、 - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("create task: handing jsonobject failed"); - } - } -/* - * 创建一个新的任务列表,并将其上传到服务器。 - * @param tasklist 要创建的任务列表对象 - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如JSON处理失败) - */ - public void createTaskList(TaskList tasklist) throws NetworkFailureException { - // 提交更新操作 - commitUpdate(); - try { - // 创建一个新的JSONObject,用于存储POST请求的数据 - JSONObject jsPost = new JSONObject(); - // 创建一个JSONArray,用于存储任务列表的创建操作 - JSONArray actionList = new JSONArray(); - - // action_list - // 将任务列表的创建操作添加到actionList中 - actionList.put(tasklist.getCreateAction(getActionId())); - // 将actionList添加到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client version - // 添加客户端版本信息到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - // post - // 发送POST请求,并将响应内容转换为JSONObject - JSONObject jsResponse = postRequest(jsPost); - // 从响应中获取结果数组,并获取第一个结果 - JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( - GTaskStringUtils.GTASK_JSON_RESULTS).get(0); - // 从结果中获取新创建任务列表的ID,并设置到任务列表对象中 - tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); - - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("create tasklist: handing jsonobject failed"); - } - } -/* - * 提交所有未完成的更新操作到服务器。 - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如JSON处理失败) - */ - public void commitUpdate() throws NetworkFailureException { - // 检查是否有未完成的更新操作 - if (mUpdateArray != null) { - try { - // 创建一个新的JSONObject,用于存储POST请求的数据 - JSONObject jsPost = new JSONObject(); - - // action_list - // 将未完成的更新操作数组添加到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); - - // client_version - // 添加客户端版本信息到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // 发送POST请求 - postRequest(jsPost); - // 提交成功后,清空更新操作数组 - mUpdateArray = null; - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("commit update: handing jsonobject failed"); - } - } - } -/* - * 将一个节点添加到更新操作数组中,并在必要时提交更新操作。 - * @param node 要添加的节点对象 - * @throws NetworkFailureException 如果网络请求失败 - */ - public void addUpdateNode(Node node) throws NetworkFailureException { - // 检查节点是否为空 - if (node != null) { - // too many update items may result in an error - // set max to 10 items - // 如果更新操作数组中已经有超过10个项目,则提交更新操作 - // 避免过多的更新操作导致错误 - if (mUpdateArray != null && mUpdateArray.length() > 10) { - commitUpdate(); - } - // 如果更新操作数组为空,则创建一个新的JSONArray - if (mUpdateArray == null) - mUpdateArray = new JSONArray(); - // 将节点的更新操作添加到更新操作数组中 - mUpdateArray.put(node.getUpdateAction(getActionId())); - } - } -/* - * 将任务从一个任务列表移动到另一个任务列表。 - * @param task 要移动的任务对象 - * @param preParent 任务的原始父任务列表 - * @param curParent 任务的新父任务列表 - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如JSON处理失败) - */ - public void moveTask(Task task, TaskList preParent, TaskList curParent) - throws NetworkFailureException { - // 提交更新操作 - commitUpdate(); - try { - // 创建一个新的JSONObject,用于存储POST请求的数据 - JSONObject jsPost = new JSONObject(); - // 创建一个JSONArray,用于存储任务的移动操作 - JSONArray actionList = new JSONArray(); - // 创建一个JSONObject,用于存储具体的移动操作 - JSONObject action = new JSONObject(); - - // action_list - // 设置移动操作的类型为"move" - action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); - // 设置移动操作的ID - action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); - // 设置要移动的任务的ID - action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); - // 如果任务在同一个任务列表中移动,并且不是第一个任务,则设置前一个兄弟任务的ID - if (preParent == curParent && task.getPriorSibling() != null) { - // put prioring_sibing_id only if moving within the tasklist and - // it is not the first one - action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); - } - // 设置任务的原始父任务列表的ID - action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); - // 设置任务的新父任务列表的ID - action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); - // 如果任务在不同的任务列表之间移动,则设置目标任务列表的ID - if (preParent != curParent) { - // put the dest_list only if moving between tasklists - action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); - } - // 将移动操作添加到actionList中 - actionList.put(action); - // 将actionList添加到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - // 添加客户端版本信息到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // 发送POST请求 - postRequest(jsPost); - - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("move task: handing jsonobject failed"); - } - } -/* - * 删除一个节点,并将其标记为已删除状态。 - * @param node 要删除的节点对象 - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如JSON处理失败) - */ - public void deleteNode(Node node) throws NetworkFailureException { - // 提交更新操作 - commitUpdate(); - try { - // 创建一个新的JSONObject,用于存储POST请求的数据 - JSONObject jsPost = new JSONObject(); - // 创建一个JSONArray,用于存储节点的删除操作 - JSONArray actionList = new JSONArray(); - - // action_list - // 将节点标记为已删除 - node.setDeleted(true); - // 获取节点的更新操作,并将其添加到actionList中 - actionList.put(node.getUpdateAction(getActionId())); - // 将actionList添加到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - // 添加客户端版本信息到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // 发送POST请求 - postRequest(jsPost); - // 提交成功后,清空更新操作数组 - mUpdateArray = null; - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("delete node: handing jsonobject failed"); - } - } -/* - * 获取所有任务列表,并返回一个包含任务列表的JSONArray。 - * @return 包含任务列表的JSONArray - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如未登录或JSON处理失败) - */ - public JSONArray getTaskLists() throws NetworkFailureException { - // 检查用户是否已登录,如果未登录则抛出异常 - if (!mLoggedin) { - Log.e(TAG, "please login first"); - throw new ActionFailureException("not logged in"); - } - - try { - // 创建一个HttpGet对象,用于发送GET请求 - HttpGet httpGet = new HttpGet(mGetUrl); - HttpResponse response = null; - // 执行GET请求,并获取响应 - response = mHttpClient.execute(httpGet); - - // get the task list - // 获取响应内容并转换为字符串 - String resString = getResponseContent(response.getEntity()); - // 从响应内容中提取包含任务列表的JSON字符串 - 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 - JSONObject js = new JSONObject(jsString); - // 从JSONObject中获取任务列表的JSONArray并返回 - return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); - } catch (ClientProtocolException e) { - // 捕获并记录HTTP协议异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("gettasklists: httpget failed"); - } catch (IOException e) { - // 捕获并记录IO异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("gettasklists: httpget failed"); - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("get task lists: handing jasonobject failed"); - } - } -/* - * 获取指定任务列表中的所有任务,并返回一个包含任务的JSONArray。 - * @param listGid 任务列表的ID - * @return 包含任务的JSONArray - * @throws NetworkFailureException 如果网络请求失败 - * @throws ActionFailureException 如果操作失败(例如JSON处理失败) - */ - public JSONArray getTaskList(String listGid) throws NetworkFailureException { - // 提交更新操作 - commitUpdate(); - try { - // 创建一个新的JSONObject,用于存储POST请求的数据 - JSONObject jsPost = new JSONObject(); - // 创建一个JSONArray,用于存储获取任务列表的操作 - JSONArray actionList = new JSONArray(); - // 创建一个JSONObject,用于存储具体的获取操作 - JSONObject action = new JSONObject(); - - // action_list - // 设置获取操作的类型为"getall" - action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); - // 设置获取操作的ID - action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); - // 设置要获取的任务列表的ID - action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); - // 设置是否获取已删除的任务,这里设置为false - action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); - // 将获取操作添加到actionList中 - actionList.put(action); - // 将actionList添加到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - // 添加客户端版本信息到jsPost中 - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // 发送POST请求,并将响应内容转换为JSONObject - JSONObject jsResponse = postRequest(jsPost); - // 从响应中获取任务列表的JSONArray并返回 - return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); - } catch (JSONException e) { - // 捕获并记录JSON解析异常 - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("get task list: handing jsonobject failed"); - } - } -/* - * 获取当前同步的账户。 - * @return 当前同步的账户对象 - */ - public Account getSyncAccount() { - return mAccount; - } -/* - * 重置更新操作数组。 - */ - public void resetUpdateArray() { - mUpdateArray = null; - } -} -- 2.34.1 From e01f8e9e8282e1b4cc05a865ef5bc68619003aa2 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:11 +0800 Subject: [PATCH 31/44] Delete 'GTaskManager.java' --- GTaskManager.java | 1065 --------------------------------------------- 1 file changed, 1065 deletions(-) delete mode 100644 GTaskManager.java diff --git a/GTaskManager.java b/GTaskManager.java deleted file mode 100644 index cf77515..0000000 --- a/GTaskManager.java +++ /dev/null @@ -1,1065 +0,0 @@ -/* - * 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. - */ -/* - * GTaskManager 负责管理本地笔记数据库与 Google Tasks 之间的同步过程。 - */ -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; - -/* - * GTaskManager 负责管理本地笔记数据库与 Google Tasks 之间的同步过程。 - */ -public class GTaskManager { - private static final String TAG = GTaskManager.class.getSimpleName(); - // 同步状态常量 - public static final int STATE_SUCCESS = 0; - - public static final int STATE_NETWORK_ERROR = 1; - - public static final int STATE_INTERNAL_ERROR = 2; - - public static final int STATE_SYNC_IN_PROGRESS = 3; - - public static final int STATE_SYNC_CANCELLED = 4; - // 单例实例 - private static GTaskManager mInstance = null; - // 用于获取认证令牌的活动上下文 - private Activity mActivity; - // 应用程序上下文 - private Context mContext; - // 内容解析器用于数据库操作 - private ContentResolver mContentResolver; - // 同步状态标志 - private boolean mSyncing; - - private boolean mCancelled; - // 用于管理任务列表、节点、元数据和 GID 与 NID 映射的哈希表 - private HashMap mGTaskListHashMap; - - private HashMap mGTaskHashMap; - - private HashMap mMetaHashMap; - - private TaskList mMetaList; - - private HashSet mLocalDeleteIdMap; - - private HashMap mGidToNid; - - private HashMap mNidToGid; - - private GTaskManager() { - // 初始化同步状态标志,表示当前没有正在进行同步 - mSyncing = false; - // 初始化取消同步标志,表示当前没有取消同步 - mCancelled = false; - // 初始化用于存储 Google Task 列表的哈希表 - mGTaskListHashMap = new HashMap();、 - // 初始化用于存储 Google Task 节点的哈希表 - mGTaskHashMap = new HashMap(); - // 初始化用于存储元数据的哈希表 - mMetaHashMap = new HashMap(); - // 初始化元数据列表,初始值为 null - mMetaList = null; - // 初始化用于存储本地删除的笔记 ID 的集合 - mLocalDeleteIdMap = new HashSet(); - // 初始化用于存储 Google Task ID 到本地笔记 ID 映射的哈希表 - mGidToNid = new HashMap(); - // 初始化用于存储本地笔记 ID 到 Google Task ID 映射的哈希表 - mNidToGid = new HashMap(); - } -/* - * 获取 GTaskManager 的单例实例。 - * 该方法使用 synchronized 关键字确保线程安全,防止多个线程同时创建实例。 - * @return GTaskManager 的单例实例 - */ - public static synchronized GTaskManager getInstance() { - // 检查单例实例是否已经创建 - if (mInstance == null) { - // 如果未创建,则创建一个新的 GTaskManager 实例 - mInstance = new GTaskManager(); - } - return mInstance; // 返回单例实例 - } -/* - * 设置活动上下文,用于获取认证令牌。 - * 该方法使用 synchronized 关键字确保线程安全,防止多个线程同时修改 mActivity 变量。 - * @param activity 活动上下文 - */ - public synchronized void setActivityContext(Activity activity) { - // used for getting authtoken - // 将传入的活动上下文赋值给 mActivity 变量 - mActivity = activity; - } -/* - * 同步本地笔记数据库与 Google Tasks。 - * 该方法负责初始化同步过程,处理同步过程中的各种异常,并返回同步结果状态。 - * @param context 应用程序上下文 - * @param asyncTask 异步任务对象,用于发布同步进度 - * @return 同步结果状态,可能的值为 STATE_SUCCESS, STATE_NETWORK_ERROR, STATE_INTERNAL_ERROR, STATE_SYNC_IN_PROGRESS, STATE_SYNC_CANCELLED - */ - 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 实例并重置更新数组 - GTaskClient client = GTaskClient.getInstance(); - client.resetUpdateArray(); - - // login google task - // 登录 Google Task - if (!mCancelled) { - if (!client.login(mActivity)) { - throw new NetworkFailureException("login google task failed"); - } - } - - // get the task list from google - // 从 Google 获取任务列表 - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); - initGTaskList(); - - // do content sync work - // 执行内容同步工作 - 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; // 返回同步结果状态 - } -/* - * 初始化 Google Task 列表。 - * 该方法从 Google Task 获取任务列表,并初始化元数据列表和任务列表。 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void initGTaskList() throws NetworkFailureException { - // 检查是否已经取消同步 - if (mCancelled) - return; - // 获取 GTaskClient 实例 - GTaskClient client = GTaskClient.getInstance(); - try { - // 从 Google Task 获取任务列表 - JSONArray jsTaskLists = client.getTaskLists(); - - // init meta list first - // 首先初始化元数据列表 - mMetaList = null; //初始化元数据列表,将其设置为 null。 - for (int i = 0; i < jsTaskLists.length(); i++) { - //遍历任务列表,逐个处理每个任务列表。 - JSONObject object = jsTaskLists.getJSONObject(i); - //获取当前任务列表的 JSONObject - String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - //获取当前任务列表的 Google Task ID - //获取当前任务列表的名称。 - String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); - // 检查是否是元数据列表 - //如果是,则初始化元数据列表并加载元数据。 - if (name - .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { - mMetaList = new TaskList(); - //创建一个新的 TaskList 对象作为元数据列表。 - mMetaList.setContentByRemoteJSON(object); - //使用远程 JSON 数据设置元数据列表的内容。 - - // load meta data - // 加载元数据 - JSONArray jsMetas = client.getTaskList(gid); - //获取元数据列表中的元数据,返回一个 JSONArray。 - for (int j = 0; j < jsMetas.length(); j++) { - //遍历元数据列表,逐个处理每个元数据。 - object = (JSONObject) jsMetas.getJSONObject(j); - MetaData metaData = new MetaData(); - //创建一个新的 MetaData 对象 - metaData.setContentByRemoteJSON(object); - //使用远程 JSON 数据设置元数据的内容 - if (metaData.isWorthSaving()) { - //检查元数据是否值得保存。如果值得保存,则将其添加到元数据列表中,并更新 mMetaHashMap - mMetaList.addChildTask(metaData); - if (metaData.getGid() != null) { - //检查元数据的 Google Task ID 是否存在。如果存在,则将其添加到 mMetaHashMap 中 - mMetaHashMap.put(metaData.getRelatedGid(), metaData); - } - } - } - } - } - - // create meta list if not existed - // 如果元数据列表不存在,则创建一个新的元数据列表 - if (mMetaList == null) { - //检查元数据列表是否为空。如果为空,则创建一个新的元数据列表并将其添加到 Google Task 中。 - mMetaList = new TaskList(); - mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META); - //设置元数据列表的名称 - GTaskClient.getInstance().createTaskList(mMetaList); - //将元数据列表添加到 Google Task 中 - } - - // init task list - // 初始化任务列表 - for (int i = 0; i < jsTaskLists.length(); i++) { - //再次遍历任务列表,逐个处理每个任务列表 - JSONObject object = jsTaskLists.getJSONObject(i); - String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); - // 检查是否是 MIUI 文件夹前缀的任务列表 - if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) - && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META)) { - //检查当前任务列表是否是 MIUI 文件夹前缀的任务列表,并且不是元数据列表。如果是,则初始化任务列表并加载任务 - TaskList tasklist = new TaskList(); - tasklist.setContentByRemoteJSON(object); - mGTaskListHashMap.put(gid, tasklist); - mGTaskHashMap.put(gid, tasklist); - - // load tasks - // 加载任务 - JSONArray jsTasks = client.getTaskList(gid); - for (int j = 0; j < jsTasks.length(); j++) { - //遍历任务列表,逐个处理每个任务。 - object = (JSONObject) jsTasks.getJSONObject(j); - gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - Task task = new Task(); - task.setContentByRemoteJSON(object); - if (task.isWorthSaving()) { - //检查任务是否值得保存。如果值得保存,则将其添加到任务列表中,并更新 mGTaskHashMap。 - 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: handing JSONObject failed"); - } - } -/* - * 同步内容。 - * 该方法负责同步本地数据库中的笔记与 Google Tasks 中的任务。 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void syncContent() throws NetworkFailureException { - int syncType; - Cursor c = null; - String gid; - Node node; - // 清空本地删除的笔记 ID 集合 - mLocalDeleteIdMap.clear(); - // 检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - if (mCancelled) { - return; - } - - // for local deleted note - // 处理本地删除的笔记 - //使用 try-finally 结构确保在查询结束后关闭游标。 - 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); - //查询本地删除的笔记。查询条件为 type 不等于 Notes.TYPE_SYSTEM 且 parent_id 等于 Notes.ID_TRASH_FOLER。 - if (c != null) { - //检查查询结果是否为空。如果不为空,则遍历查询结果。 - while (c.moveToNext()) { - //遍历查询结果,逐个处理每个笔记。 - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - //获取当前笔记的 Google Task ID。 - node = mGTaskHashMap.get(gid); - //从 mGTaskHashMap 中获取对应的节点。 - if (node != null) { - //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并执行远程删除操作。 - mGTaskHashMap.remove(gid); - doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); - //执行远程删除操作。 - } - - mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); - //将当前笔记的本地 ID 添加到 mLocalDeleteIdMap 中。 - } - } else { - Log.w(TAG, "failed to query trash folder"); - } - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // sync folder first - syncFolder(); - - // for note existing in database - // 处理数据库中存在的笔记 - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type=? AND parent_id<>?)", new String[] { - String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) - }, NoteColumns.TYPE + " DESC"); - //查询数据库中存在的笔记。查询条件为 type 等于 Notes.TYPE_NOTE 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 - if (c != null) { - //检查查询结果是否为空。如果不为空,则遍历查询结果。 - while (c.moveToNext()) { - //遍历查询结果,逐个处理每个笔记。 - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - //获取当前笔记的 Google Task ID。 - node = mGTaskHashMap.get(gid); - //从 mGTaskHashMap 中获取对应的节点。 - if (node != null) { - //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射,然后获取同步操作类型。 - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); - //更新 Google Task ID 到本地笔记 ID 的映射。 - mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); - //更新本地笔记 ID 到 Google Task ID 的映射。 - syncType = node.getSyncAction(c); - //获取当前笔记的同步操作类型。 - } else { - //如果节点不存在,则根据 Google Task ID 是否为空来确定同步操作类型。 - if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add - // 本地添加 - //检查 Google Task ID 是否为空。如果为空,则表示本地添加操作。 - syncType = Node.SYNC_ACTION_ADD_REMOTE; - //设置同步操作类型为远程添加。 - } else { - // remote delete - // 远程删除 - syncType = Node.SYNC_ACTION_DEL_LOCAL; - } - } - doContentSync(syncType, node, c);//执行同步操作。 - } - } else { - Log.w(TAG, "failed to query existing note in database"); - } - - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // go through remaining items - // 处理剩余的项目 - Iterator> iter = mGTaskHashMap.entrySet().iterator(); - //获取 mGTaskHashMap 的迭代器。 - while (iter.hasNext()) { - //遍历 mGTaskHashMap,逐个处理剩余的项目。 - Map.Entry entry = iter.next(); - //获取当前的键值对。 - node = entry.getValue(); - doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); - //执行本地添加操作 - } - - // mCancelled can be set by another thread, so we neet to check one by - // one - // clear local delete table - // 检查是否已经取消同步 - if (!mCancelled) { - // 批量删除本地删除的笔记 - if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { - throw new ActionFailureException("failed to batch-delete local deleted notes"); - } - //批量删除本地删除的笔记。如果删除失败,则抛出 ActionFailureException。 - } - - // refresh local sync id - // 刷新本地同步 ID - if (!mCancelled) { - GTaskClient.getInstance().commitUpdate(); - refreshLocalSyncId(); - } - - } -/* - * 同步文件夹。 - * 该方法负责同步本地数据库中的文件夹与 Google Tasks 中的任务列表。 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void syncFolder() throws NetworkFailureException { - Cursor c = null; - String gid; - Node node; - int syncType; - //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - if (mCancelled) { - return; - } - - // for root folder - //处理根文件夹。 - try { - c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, - Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); - //查询根文件夹。查询条件为 _id 等于 Notes.ID_ROOT_FOLDER。 - if (c != null) { - //检查查询结果是否为空。如果不为空,则处理查询结果。 - c.moveToNext(); - //移动游标到下一行。 - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - //获取当前文件夹的 Google Task ID。 - node = mGTaskHashMap.get(gid); - //从 mGTaskHashMap 中获取对应的节点。 - if (node != null) { - //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射。 - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); - //更新 Google Task ID 到本地笔记 ID 的映射。 - mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); - // for system folder, only update remote name if necessary - //更新本地笔记 ID 到 Google Task ID 的映射。 - 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; - } - } - - // for call-note folder - //处理通话记录文件夹。 - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(Notes.ID_CALL_RECORD_FOLDER) - }, null); - //查询通话记录文件夹。查询条件为 _id 等于 Notes.ID_CALL_RECORD_FOLDER。 - if (c != null) { - //检查查询结果是否为空。如果不为空,则处理查询结果。 - if (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射。 - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); - mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); - // for system folder, only update remote name if - // necessary - if (!node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_CALL_NOTE)) - 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; - } - } - - // for local existing folders - //处理本地存在的文件夹。 - 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"); - //查询本地存在的文件夹。查询条件为 type 等于 Notes.TYPE_FOLDER 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 - if (c != null) { - //检查查询结果是否为空。如果不为空,则遍历查询结果。 - while (c.moveToNext()) { - //遍历查询结果,逐个处理每个文件夹。 - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射,然后获取同步操作类型。 - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); - mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); - syncType = node.getSyncAction(c); - } else { - //如果节点不存在,则根据 Google Task ID 是否为空来确定同步操作类型。 - if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add - //检查 Google Task ID 是否为空。如果为空,则表示本地添加操作 - syncType = Node.SYNC_ACTION_ADD_REMOTE; - } else { - //如果 Google Task ID 不为空,则表示远程删除操作。 - // remote delete - 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; - } - } - - // for remote add folders - Iterator> iter = mGTaskListHashMap.entrySet().iterator(); - //获取 mGTaskListHashMap 的迭代器。 - while (iter.hasNext()) { - //遍历 mGTaskListHashMap,逐个处理远程添加的文件夹。 - Map.Entry entry = iter.next(); - //获取当前的键值对 - gid = entry.getKey(); - node = entry.getValue(); - if (mGTaskHashMap.containsKey(gid)) { - //检查 mGTaskHashMap 中是否包含该节点。如果包含,则从 mGTaskHashMap 中移除该节点,并执行本地添加操作。 - 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: - // 本地删除操作。从 mMetaHashMap 中获取元数据。如果元数据存在,则调用 GTaskClient.getInstance().deleteNode(meta) 方法删除元数据。将本地笔记 ID 添加到 mLocalDeleteIdMap 中。 - meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); - if (meta != null) { - GTaskClient.getInstance().deleteNode(meta); - } - mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); - break; - case Node.SYNC_ACTION_DEL_REMOTE: - // 远程删除操作。从 mMetaHashMap 中获取元数据。如果元数据存在,则调用 GTaskClient.getInstance().deleteNode(meta) 方法删除元数据。调用 GTaskClient.getInstance().deleteNode(node) 方法删除节点。 - 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: - // merging both modifications maybe a good idea - // right now just use local update simply - // 更新冲突操作 - // 合并两个修改可能是一个好主意 - // 现在只是简单地使用本地更新 - updateRemoteNode(node, c); - break; - case Node.SYNC_ACTION_NONE: - // 无操作 - break; - case Node.SYNC_ACTION_ERROR: - default: - // 未知同步操作类型 - throw new ActionFailureException("unkown sync action type"); - } - } -/* - * 添加本地节点。 - * 该方法负责将远程节点添加到本地数据库中。 - * @param node 远程节点对象 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void addLocalNode(Node node) throws NetworkFailureException { - if (mCancelled) { - //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - return; - } - - SqlNote sqlNote;//声明一个 SqlNote 变量,用于存储本地笔记。 - if (node instanceof TaskList) { - //检查节点是否是任务列表。如果是任务列表,则根据节点的名称创建相应的本地笔记。 - if (node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { - //检查节点名称是否是根文件夹。如果是根文件夹,则创建根文件夹的本地笔记。 - sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); - } else if (node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { - //检查节点名称是否是通话记录文件夹。如果是通话记录文件夹,则创建通话记录文件夹的本地笔记。 - sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); - } else { - //其他文件夹。创建新的本地笔记,并设置内容和父节点 ID。 - sqlNote = new SqlNote(mContext); - sqlNote.setContent(node.getLocalJSONFromContent()); - sqlNote.setParentId(Notes.ID_ROOT_FOLDER); - } - } else { - //如果是任务。创建新的本地笔记,并设置内容和父节点 ID。 - sqlNote = new SqlNote(mContext); - JSONObject js = node.getLocalJSONFromContent(); - try { - //处理 JSON 解析异常。 - if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { - //检查是否包含笔记信息。如果包含笔记信息,则检查笔记 ID 是否可用。 - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - if (note.has(NoteColumns.ID)) { - //检查笔记信息是否包含 ID。如果包含 ID,则检查 ID 是否可用。 - long id = note.getLong(NoteColumns.ID); - if (DataUtils.existInNoteDatabase(mContentResolver, id)) { - // the id is not available, have to create a new one - //检查笔记 ID 是否存在于本地数据库中。如果存在,则移除笔记 ID。 - note.remove(NoteColumns.ID); - } - } - } - - if (js.has(GTaskStringUtils.META_HEAD_DATA)) { - //检查是否包含数据信息。如果包含数据信息,则检查数据 ID 是否可用。 - 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)) { - //检查数据信息是否包含 ID。如果包含 ID,则检查 ID 是否可用。 - long dataId = data.getLong(DataColumns.ID); - if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { - // the data id is not available, have to create - // a new one - //检查数据 ID 是否存在于本地数据库中。如果存在,则移除数据 ID。 - data.remove(DataColumns.ID); - } - } - } - - } - } catch (JSONException e) { - Log.w(TAG, e.toString()); - e.printStackTrace(); - } - sqlNote.setContent(js);//设置本地笔记的内容。 - - Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); - //获取父节点的本地 ID。 - if (parentId == null) { - //检查父节点的本地 ID 是否存在。如果不存在,则抛出 ActionFailureException 异常。 - Log.e(TAG, "cannot find task's parent id locally"); - throw new ActionFailureException("cannot add local node"); - } - sqlNote.setParentId(parentId.longValue()); - } - - // create the local node - sqlNote.setGtaskId(node.getGid());//更新 Google Task ID 到本地笔记 ID 的映射。 - sqlNote.commit(false); - - // update gid-nid mapping - mGidToNid.put(node.getGid(), sqlNote.getId());//更新本地笔记 ID 到 Google Task ID 的映射。 - mNidToGid.put(sqlNote.getId(), node.getGid()); - - // update meta - updateRemoteMeta(node.getGid(), sqlNote);//更新元数据。 - } -/* - * 更新本地节点。 - * 该方法负责将远程节点的更新同步到本地数据库中。 - * @param node 远程节点对象 - * @param c 游标对象 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - return; - } - - SqlNote sqlNote; - // update the note locally - //声明一个 SqlNote 变量,用于存储本地笔记。 - sqlNote = new SqlNote(mContext, c); - //创建一个新的 SqlNote 对象,并使用游标 c 初始化。 - sqlNote.setContent(node.getLocalJSONFromContent()); - //设置本地笔记的内容为远程节点的本地 JSON 内容。 - Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) - : new Long(Notes.ID_ROOT_FOLDER); - //获取父节点的本地 ID。如果节点是任务,则从 mGidToNid 映射中获取父节点的本地 ID。如果节点不是任务,则将父节点 ID 设置为根文件夹的 ID。 - if (parentId == null) { - //检查父节点的本地 ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 - Log.e(TAG, "cannot find task's parent id locally"); - throw new ActionFailureException("cannot update local node"); - } - sqlNote.setParentId(parentId.longValue());//设置本地笔记的父节点 ID。 - sqlNote.commit(true);//提交本地笔记的更新。 - - // update meta info - //更新元数据信息。 - updateRemoteMeta(node.getGid(), sqlNote); - } -/* - * 添加远程节点。 - * 该方法负责将本地笔记添加到 Google Tasks 中。 - * @param node 远程节点对象 - * @param c 游标对象 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - return; - } - - SqlNote sqlNote = new SqlNote(mContext, c); - //创建一个新的 SqlNote 对象,并使用游标 c 初始化。 - Node n; - //声明一个 Node 变量,用于存储远程节点 - - // update remotely - if (sqlNote.isNoteType()) { - //检查本地笔记是否是笔记类型。如果是笔记类型,则创建一个新的 Task 对象,并设置内容。 - Task task = new Task(); - task.setContentByLocalJSON(sqlNote.getContent()); - - String parentGid = mNidToGid.get(sqlNote.getParentId()); - if (parentGid == null) { - //检查父任务列表的 Google Task ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 - 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; - //将 Task 对象赋值给 Node 变量。 - - // add meta - //添加元数据。 - updateRemoteMeta(task.getGid(), sqlNote); - } else { - //如果是文件夹类型。创建一个新的 TaskList 对象,并设置内容。 - TaskList tasklist = null; - - // we need to skip folder if it has already existed - String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;//初始化文件夹名称 - if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) - //检查本地笔记的 ID 是否是根文件夹的 ID。如果是根文件夹,则设置文件夹名称为默认文件夹名称。 - folderName += GTaskStringUtils.FOLDER_DEFAULT; - else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) - //检查本地笔记的 ID 是否是通话记录文件夹的 ID。如果是通话记录文件夹,则设置文件夹名称为通话记录文件夹名称。 - folderName += GTaskStringUtils.FOLDER_CALL_NOTE; - else - //其他文件夹。设置文件夹名称为本地笔记的片段。 - folderName += sqlNote.getSnippet(); - - Iterator> iter = mGTaskListHashMap.entrySet().iterator(); - while (iter.hasNext()) { - //遍历 mGTaskListHashMap,查找匹配的文件夹。 - Map.Entry entry = iter.next(); - String gid = entry.getKey(); - TaskList list = entry.getValue(); - - if (list.getName().equals(folderName)) { - //检查任务列表的名称是否与文件夹名称匹配。如果匹配,则将任务列表赋值给 tasklist 变量,并从 mGTaskHashMap 中移除该节点。 - tasklist = list; - if (mGTaskHashMap.containsKey(gid)) { - //检查 mGTaskHashMap 中是否包含该节点。如果包含,则从 mGTaskHashMap 中移除该节点。 - mGTaskHashMap.remove(gid); - } - break; - } - } - - // no match we can add now - if (tasklist == null) { - //如果没有匹配的文件夹,则可以添加。创建一个新的 TaskList 对象,并设置内容。 - tasklist = new TaskList(); - tasklist.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().createTaskList(tasklist); - mGTaskListHashMap.put(tasklist.getGid(), tasklist); - } - n = (Node) tasklist;//将 TaskList 对象赋值给 Node 变量。 - } - - // update local note - sqlNote.setGtaskId(n.getGid());//设置本地笔记的 Google Task ID - sqlNote.commit(false);//提交本地笔记的更新。 - sqlNote.resetLocalModified();//重置本地笔记的修改标志。 - sqlNote.commit(true);//重置本地笔记的修改标志。 - - // gid-id mapping - mGidToNid.put(n.getGid(), sqlNote.getId());//更新 Google Task ID 到本地笔记 ID 的映射。 - mNidToGid.put(sqlNote.getId(), n.getGid());//更新本地笔记 ID 到 Google Task ID 的映射。 - } -/* - * 更新远程节点。 - * 该方法负责将本地笔记的更新同步到 Google Tasks 中。 - * @param node 远程节点对象 - * @param c 游标对象 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - return; - } - - SqlNote sqlNote = new SqlNote(mContext, c);//创建一个新的 SqlNote 对象,并使用游标 c 初始化。 - - // update remotely - //使用本地笔记的内容更新远程节点的内容。 - node.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().addUpdateNode(node); - //将更新后的节点添加到 GTaskClient 的更新列表中。 - - // update meta - //更新元数据。 - updateRemoteMeta(node.getGid(), sqlNote); - - // move task if necessary - if (sqlNote.isNoteType()) { - //检查本地笔记是否是笔记类型。如果是笔记类型,则处理任务的移动 - Task task = (Task) node; - TaskList preParentList = task.getParent(); - - String curParentGid = mNidToGid.get(sqlNote.getParentId()); - if (curParentGid == null) { - //检查父任务列表的 Google Task ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 - 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); - } - } - - // clear local modified flag - sqlNote.resetLocalModified();//清除本地笔记的修改标志。 - sqlNote.commit(true);//提交本地笔记的更新。 - } -/* - * 更新远程元数据。 - * 该方法负责将本地笔记的元数据同步到 Google Tasks 中。 - * @param gid Google Task ID - * @param 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。 - * 该方法负责刷新本地笔记的同步 ID,确保本地笔记的同步 ID 与 Google Tasks 中的最新数据一致。 - * @throws NetworkFailureException 如果网络操作失败 - */ - private void refreshLocalSyncId() throws NetworkFailureException { - // 检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 - if (mCancelled) { - return; - } - - // get the latest gtask list - mGTaskHashMap.clear();//清空 mGTaskHashMap - mGTaskListHashMap.clear();//清空 mGTaskListHashMap。 - mMetaHashMap.clear();//清空 mMetaHashMap。 - initGTaskList();//初始化 Google Task 列表。 - - Cursor c = null;//声明一个游标变量 c,用于存储查询结果。 - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type<>? AND parent_id<>?)", new String[] { - //查询本地笔记。查询条件为 type 不等于 Notes.TYPE_SYSTEM 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 - 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 中移除该节点,并更新本地笔记的同步 ID。 - 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 { - //如果节点不存在,则记录错误日志并抛出 ActionFailureException 异常。 - Log.e(TAG, "something is missed"); - throw new ActionFailureException( - "some local items don't have gid after sync"); - } - } - } else { - Log.w(TAG, "failed to query local note to refresh sync id"); - } - } finally { - if (c != null) { - //检查游标是否为空。如果不为空,则关闭游标。 - c.close(); - c = null; - } - } - } -/* - * 获取同步账户名称。 - * 该方法返回当前用于同步的 Google 账户名称。 - * @return 同步账户名称 - */ - public String getSyncAccount() { - // 获取 GTaskClient 的单例实例,并返回同步账户的名称 - return GTaskClient.getInstance().getSyncAccount().name; - } -/* - * 取消同步。 - * 该方法用于设置取消同步标志,以便在同步过程中停止同步操作。 - */ - public void cancelSync() { - // 设置取消同步标志为 true - mCancelled = true; - } -} -- 2.34.1 From 50288ef416ab1c7832e3176f5b903100023ece2a Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:20 +0800 Subject: [PATCH 32/44] Delete 'GTaskSyncService.java' --- GTaskSyncService.java | 198 ------------------------------------------ 1 file changed, 198 deletions(-) delete mode 100644 GTaskSyncService.java diff --git a/GTaskSyncService.java b/GTaskSyncService.java deleted file mode 100644 index 7ba4834..0000000 --- a/GTaskSyncService.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; -/* - * GTaskSyncService类是一个服务类,用于处理Google任务的同步操作。 - * 该类继承自Service,并在后台执行同步任务。 - */ -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) { - // 创建一个新的GTaskASyncTask对象 - mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { - public void onComplete() { - // 同步任务完成后,将mSyncTask置为null - mSyncTask = null; - // 发送广播通知同步完成 - sendBroadcast(""); - // 停止服务 - stopSelf(); - } - }); - // 发送广播通知同步开始 - sendBroadcast(""); - // 执行同步任务 - mSyncTask.execute(); - } - } -/* - * 取消同步操作。 - */ - private void cancelSync() { - // 如果同步任务正在执行 - if (mSyncTask != null) { - // 调用cancelSync方法取消同步 - mSyncTask.cancelSync(); - } - } -/* - * 服务创建时调用,初始化同步任务。 - */ - @Override - public void onCreate() { - // 将同步任务置为null - mSyncTask = null; - } -/* - * 服务启动时调用,处理启动和取消同步的命令。 - * @param intent 启动服务的意图 - * @param flags 启动标志 - * @param startId 启动ID - * @return 服务启动模式 - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // 获取意图中的额外数据 - Bundle bundle = intent.getExtras(); - // 如果额外数据不为空且包含ACTION_STRING_NAME - if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { - // 根据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; - } - // 返回START_STICKY,表示服务在异常终止后会自动重启 - return START_STICKY; - } - // 如果额外数据为空或不包含ACTION_STRING_NAME,调用父类的onStartCommand方法 - return super.onStartCommand(intent, flags, startId); - } -/* - * 内存不足时调用,取消同步任务。 - */ - @Override - public void onLowMemory() { - // 如果同步任务正在执行 - if (mSyncTask != null) { - // 调用cancelSync方法取消同步 - mSyncTask.cancelSync(); - } - } -/* - * 绑定服务时调用,返回null。 - * @param intent 绑定服务的意图 - * @return null - */ - public IBinder onBind(Intent intent) { - // 返回null,表示不支持绑定服务 - return null; - } -/* - * 发送广播,通知同步状态和进度。 - * @param msg 进度消息 - */ - public void sendBroadcast(String msg) { - // 更新同步进度字符串 - mSyncProgress = msg; - // 创建一个新的Intent对象,用于发送广播 - 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); - } -/* - * 启动同步操作的静态方法。 - * @param activity 启动同步的Activity - */ - public static void startSync(Activity activity) { - // 设置活动上下文 - GTaskManager.getInstance().setActivityContext(activity); - // 创建一个新的Intent对象,用于启动服务 - Intent intent = new Intent(activity, GTaskSyncService.class); - // 添加启动同步的命令 - intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); - // 启动服务 - activity.startService(intent); - } -/* - * 取消同步操作的静态方法。 - * @param context 取消同步的上下文 - */ - public static void cancelSync(Context context) { - // 创建一个新的Intent对象,用于启动服务 - Intent intent = new Intent(context, GTaskSyncService.class); - // 添加取消同步的命令 - intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); - // 启动服务 - context.startService(intent); - } -/* - * 检查是否正在同步。 - * @return 如果正在同步,返回true;否则返回false - */ - public static boolean isSyncing() { - // 如果mSyncTask不为null,表示正在同步 - return mSyncTask != null; - } -/* - * 获取同步进度字符串。 - * @return 同步进度字符串 - */ - public static String getProgressString() { - // 返回当前的同步进度字符串 - return mSyncProgress; - } -} -- 2.34.1 From da25e1d40954dfb3ccc7061c360f819eb64581b5 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:27 +0800 Subject: [PATCH 33/44] Delete 'MetaData.java' --- MetaData.java | 115 -------------------------------------------------- 1 file changed, 115 deletions(-) delete mode 100644 MetaData.java diff --git a/MetaData.java b/MetaData.java deleted file mode 100644 index 1f73277..0000000 --- a/MetaData.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.tool.GTaskStringUtils; - -import org.json.JSONException; -import org.json.JSONObject; - -/* - *MetaData 类继承自 Task 类,用于处理与 Google Task 相关的元数据。 - * 该类主要用于存储和获取与 Google Task 相关的元数据信息。 - */ -public class MetaData extends Task { - private final static String TAG = MetaData.class.getSimpleName(); - - private String mRelatedGid = null; // 相关 Google Task 的 ID -/* - * 设置元数据信息。 - * 该方法用于将 Google Task 的 ID 和元数据信息存储到任务的备注字段中。 - * @param gid Google Task 的 ID。 - * @param metaInfo 元数据信息的 JSON 对象。 - */ - public void setMeta(String gid, JSONObject metaInfo) { - try { - metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); // 将 Google Task 的 ID 添加到元数据信息中 - } catch (JSONException e) { - Log.e(TAG, "failed to put related gid"); // 记录日志,添加失败 - } - setNotes(metaInfo.toString()); // 将元数据信息存储到任务的备注字段中 - setName(GTaskStringUtils.META_NOTE_NAME); // 设置任务的名称为元数据名称 - } -/* - * 获取相关 Google Task 的 ID。 - * @return 返回相关 Google Task 的 ID。 - */ - public String getRelatedGid() { - return mRelatedGid; - } -/* - * 判断任务是否值得保存。 - * 该方法用于判断任务是否包含有效的备注信息。 - * @return 如果任务包含有效的备注信息,则返回 true,否则返回 false。 - */ - @Override - public boolean isWorthSaving() { - return getNotes() != null; - } -/* - * 根据远程 JSON 对象设置任务内容。 - * 该方法用于从远程 JSON 对象中提取元数据信息,并设置相关 Google Task 的 ID。 - * @param js 远程 JSON 对象。 - */ - @Override - public void setContentByRemoteJSON(JSONObject js) { - super.setContentByRemoteJSON(js); // 调用父类方法设置任务内容 - if (getNotes() != null) { - try { - JSONObject metaInfo = new JSONObject(getNotes().trim()); // 解析备注字段中的元数据信息 - mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); // 获取相关 Google Task 的 ID - } catch (JSONException e) { - Log.w(TAG, "failed to get related gid"); // 记录日志,获取失败 - mRelatedGid = null; - } - } - } -/* - * 根据本地 JSON 对象设置任务内容。 - * 该方法不应被调用,因为元数据任务不应从本地 JSON 对象中设置内容。 - * @param js 本地 JSON 对象。 - */ - @Override - public void setContentByLocalJSON(JSONObject js) { - // this function should not be called - // 该方法不应被调用 - throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); - } -/* - * 从任务内容中获取本地 JSON 对象。 - * 该方法不应被调用,因为元数据任务不应生成本地 JSON 对象。 - * @return 抛出异常,表示该方法不应被调用。 - */ - @Override - public JSONObject getLocalJSONFromContent() { - throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); - } -/* - * 获取同步操作。 - * 该方法不应被调用,因为元数据任务不应进行同步操作。 - * @param c 数据库游标。 - * @return 抛出异常,表示该方法不应被调用。 - */ - @Override - public int getSyncAction(Cursor c) { - throw new IllegalAccessError("MetaData:getSyncAction should not be called"); - } - -} -- 2.34.1 From d889fd9a8094b427c3f2852378801bc7005c6aa9 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:38 +0800 Subject: [PATCH 34/44] Delete 'NetworkFailureException.java' --- NetworkFailureException.java | 43 ------------------------------------ 1 file changed, 43 deletions(-) delete mode 100644 NetworkFailureException.java diff --git a/NetworkFailureException.java b/NetworkFailureException.java deleted file mode 100644 index d64d13b..0000000 --- a/NetworkFailureException.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.exception; -/* - * 自定义异常类,表示网络操作失败。 - * 继承自Exception,用于在网络操作失败时抛出异常。 - */ -public class NetworkFailureException extends Exception { - private static final long serialVersionUID = 2107610287180234136L; - - public NetworkFailureException() { //默认构造函数。 - super(); - } -/* - * 带错误信息的构造函数。 - * @param paramString 错误信息 - */ - public NetworkFailureException(String paramString) { - super(paramString); - } -/* - * 带错误信息和原因的构造函数。 - * @param paramString 错误信息 - * @param paramThrowable 原因 - */ - public NetworkFailureException(String paramString, Throwable paramThrowable) { - super(paramString, paramThrowable); - } -} -- 2.34.1 From 1b1d96063294e29fd82eb1a6042b235bd3a6b20c Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:47 +0800 Subject: [PATCH 35/44] Delete 'Node.java' --- Node.java | 158 ------------------------------------------------------ 1 file changed, 158 deletions(-) delete mode 100644 Node.java diff --git a/Node.java b/Node.java deleted file mode 100644 index 670361c..0000000 --- a/Node.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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 org.json.JSONObject; -/* - * Node 类是一个抽象类,用于表示 Google Task 中的节点。 - * 该类定义了节点的基本属性和操作,并提供了抽象方法供子类实现。 - */ -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; // Google Task 的 ID - - private String mName; // 节点名称 - - private long mLastModified; // 最后修改时间 - - private boolean mDeleted; // 是否已删除 -/* - * 构造函数,初始化节点属性。 - */ - public Node() { - mGid = null; - mName = ""; - mLastModified = 0; - mDeleted = false; - } -/* - * 获取创建操作的 JSON 对象。 - * 该方法用于生成创建操作的 JSON 对象。 - * @param actionId 操作 ID。 - * @return 返回创建操作的 JSON 对象。 - */ - public abstract JSONObject getCreateAction(int actionId); -/* - * 获取更新操作的 JSON 对象。 - * 该方法用于生成更新操作的 JSON 对象。 - * @param actionId 操作 ID。 - * @return 返回更新操作的 JSON 对象。 - */ - public abstract JSONObject getUpdateAction(int actionId); -/* - * 根据远程 JSON 对象设置节点内容。 - * 该方法用于从远程 JSON 对象中提取节点内容。 - * @param js 远程 JSON 对象。 - */ - public abstract void setContentByRemoteJSON(JSONObject js); -/* - * 根据本地 JSON 对象设置节点内容。 - * 该方法用于从本地 JSON 对象中提取节点内容。 - * @param js 本地 JSON 对象。 - */ - public abstract void setContentByLocalJSON(JSONObject js); -/* - *从节点内容中获取本地 JSON 对象。 - * 该方法用于将节点内容转换为本地 JSON 对象。 - * @return 返回本地 JSON 对象。 - */ - public abstract JSONObject getLocalJSONFromContent(); -/* - * 获取同步操作。 - * 该方法用于根据数据库游标获取同步操作。 - * @param c 数据库游标。 - * @return 返回同步操作。 - */ - public abstract int getSyncAction(Cursor c); -/* - * 设置 Google Task 的 ID。 - * @param gid Google Task 的 ID。 - */ - public void setGid(String gid) { - this.mGid = gid; - } -/* - * 设置节点名称。 - * @param name 节点名称。 - */ - public void setName(String name) { - this.mName = name; - } -/* - * 设置最后修改时间。 - * @param lastModified 最后修改时间。 - */ - public void setLastModified(long lastModified) { - this.mLastModified = lastModified; - } -/* - * 设置节点是否已删除。 - * @param deleted 是否已删除。 - */ - public void setDeleted(boolean deleted) { - this.mDeleted = deleted; - } -/* - * 获取 Google Task 的 ID - * @return 返回 Google Task 的 ID。 - */ - public String getGid() { - return this.mGid; - } -/* - * 获取节点名称。 - * @return 返回节点名称。 - */ - public String getName() { - return this.mName; - } -/* - * 获取最后修改时间。 - * @return 返回最后修改时间。 - */ - public long getLastModified() { - return this.mLastModified; - } -/* - * 获取节点是否已删除。 - * @return 返回节点是否已删除。 - */ - public boolean getDeleted() { - return this.mDeleted; - } - -} -- 2.34.1 From 6c5db2212d33a97a8d4bc8b48c10f9f291bcbbe5 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:18:54 +0800 Subject: [PATCH 36/44] Delete 'Note.java' --- Note.java | 382 ------------------------------------------------------ 1 file changed, 382 deletions(-) delete mode 100644 Note.java diff --git a/Note.java b/Note.java deleted file mode 100644 index 27b9dab..0000000 --- a/Note.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.model; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.OperationApplicationException; -import android.net.Uri; -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.DataColumns; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.Notes.TextNote; - -import java.util.ArrayList; - -/* - * Note 类用于管理笔记的创建、更新和同步。 - * 该类提供了创建新笔记、设置笔记内容、同步笔记等功能。 - */ -public class Note { - private ContentValues mNoteDiffValues; - private NoteData mNoteData; - private static final String TAG = "Note"; - /** - * Create a new note id for adding a new note to databases - */ - /* - * 创建一个新的笔记 ID,用于将新笔记添加到数据库中。 - * - * @param context 应用程序上下文 - * @param folderId 文件夹 ID - * @return 新创建的笔记 ID - */ - public static synchronized long getNewNoteId(Context context, long folderId) { - // Create a new note in the database - // 创建一个新的笔记到数据库中 - ContentValues values = new ContentValues(); - //创建一个新的 ContentValues 对象,用于存储笔记的初始值。 - long createdTime = System.currentTimeMillis(); - //获取当前时间戳,作为笔记的创建时间和修改时间。 - values.put(NoteColumns.CREATED_DATE, createdTime); - //将创建时间存储到 ContentValues 对象中。 - values.put(NoteColumns.MODIFIED_DATE, createdTime); - //将修改时间存储到 ContentValues 对象中。 - values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - //将笔记类型存储到 ContentValues 对象中。 - values.put(NoteColumns.LOCAL_MODIFIED, 1); - //将本地修改标志设置为 1,表示笔记已被修改。 - values.put(NoteColumns.PARENT_ID, folderId); - //将文件夹 ID 存储到 ContentValues 对象中。 - Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); - //将 ContentValues 对象插入到数据库中,并返回新创建笔记的 URI。 - long noteId = 0;//初始化笔记 ID 变量。 - try { - //尝试从 URI 中获取笔记 ID。如果发生 NumberFormatException 异常,则记录错误日志,并将笔记 ID 设置为 0。 - noteId = Long.valueOf(uri.getPathSegments().get(1)); - } catch (NumberFormatException e) { - Log.e(TAG, "Get note id error :" + e.toString()); - noteId = 0; - } - if (noteId == -1) { - //检查笔记 ID 是否为 -1。如果为 -1,则抛出 IllegalStateException 异常。 - throw new IllegalStateException("Wrong note id:" + noteId); - } - return noteId;//返回新创建的笔记 ID。 - } -/* - * 构造函数,初始化笔记的差异值和笔记数据。 - */ - public Note() { - //// 初始化笔记的差异值 - mNoteDiffValues = new ContentValues(); - // // 初始化笔记数据 - mNoteData = new NoteData(); - } -/* - * 设置笔记的差异值。 - * @param key 键 - * @param value 值 - */ - public void setNoteValue(String key, String value) { - // 将键值对存储到笔记的差异值中 - mNoteDiffValues.put(key, value); - // 设置本地修改标志为 1,表示笔记已被修改 - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - // 设置修改时间为当前时间 - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); - } -/* - * 设置笔记的文本数据。 - * @param key 键 - * @param value 值 - */ - public void setTextData(String key, String value) { - // 调用 NoteData 类的 setTextData 方法,设置文本数据 - mNoteData.setTextData(key, value); - } -/* - * 设置笔记的文本数据 ID。 - * @param id 文本数据 ID - */ - public void setTextDataId(long id) { - // 调用 NoteData 类的 setTextDataId 方法,设置文本数据 ID - mNoteData.setTextDataId(id); - } -/* - * 获取笔记的文本数据 ID。 - * @return 文本数据 ID - */ - public long getTextDataId() { - // 返回 NoteData 类的 mTextDataId 字段 - return mNoteData.mTextDataId; - } -/* - * 设置笔记的通话数据 ID。 - * @param id 通话数据 ID - */ - public void setCallDataId(long id) { - // 调用 NoteData 类的 setCallDataId 方法,设置通话数据 ID - mNoteData.setCallDataId(id); - } -/* - * 设置调用数据的方法。 - * 该方法接受两个参数:一个键和一个值,并将它们传递给 `mNoteData` 对象的 `setCallData` 方法。 - * @param key 用于存储数据的键。 - * @param value 与键关联的值。 - */ - public void setCallData(String key, String value) { - //调用 `mNoteData` 对象的 `setCallData` 方法,并将 `key` 和 `value` 作为参数传递。 - mNoteData.setCallData(key, value); - } -/* - * 检查是否有本地修改的方法。 - * 该方法返回一个布尔值,指示是否有本地修改。如果有本地修改,则返回 true;否则返回 false。 - * 本地修改的判断基于两个条件: - * 1. `mNoteDiffValues` 集合的大小是否大于 0。 - * 2. `mNoteData` 对象是否报告本地修改。 - * @return 如果有本地修改,则返回 true;否则返回 false。 - */ - public boolean isLocalModified() { - // 检查 `mNoteDiffValues` 集合的大小是否大于 0,或者 `mNoteData` 对象是否报告本地修改。 - return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); - } -/* - * 同步笔记的方法。 - * 该方法用于将本地修改的笔记数据同步到内容提供者中。 - * @param context 应用程序上下文。 - * @param noteId 笔记的唯一标识符。 - * @return 如果同步成功,则返回 true;否则返回 false。 - * @throws IllegalArgumentException 如果 noteId 小于或等于 0,则抛出此异常。 - */ - public boolean syncNote(Context context, long noteId) { - /// 检查 noteId 是否有效,如果无效则抛出异常。 - if (noteId <= 0) { - throw new IllegalArgumentException("Wrong note id:" + noteId); - } - // 如果没有本地修改,则直接返回 true。 - if (!isLocalModified()) { - return true; - } - - /** - * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and - * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the - * note data info - * 理论上,一旦数据发生变化,笔记应该在 {@link NoteColumns#LOCAL_MODIFIED} 和 - * {@link NoteColumns#MODIFIED_DATE} 上更新。为了数据安全,即使更新笔记失败,我们也会更新笔记数据信息。 - */ - // 尝试更新笔记数据。 - if (context.getContentResolver().update( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, - null) == 0) { - // 如果更新失败,记录错误日志。 - Log.e(TAG, "Update note error, should not happen"); - // Do not return, fall through - // 不返回,继续执行。 - } - // 清空 mNoteDiffValues 集合。 - mNoteDiffValues.clear(); - // 如果 mNoteData 有本地修改,并且推送数据到内容提供者失败,则返回 false。 - if (mNoteData.isLocalModified() - && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { - return false; - } - // 如果所有操作成功,则返回 true。 - return true; - } -/* - * 笔记数据类,用于管理笔记的文本数据和通话数据。 - */ - private class NoteData { - private long mTextDataId;// 文本数据的唯一标识符 - - private ContentValues mTextDataValues;// 文本数据的 ContentValues 对象 - - private long mCallDataId; // 通话数据的唯一标识符 - - private ContentValues mCallDataValues; // 通话数据的 ContentValues 对象 - - private static final String TAG = "NoteData";// 日志标签 -/* - * 构造函数,初始化文本数据和通话数据的 ContentValues 对象,并将数据 ID 设置为 0。 - */ - public NoteData() { - mTextDataValues = new ContentValues(); - mCallDataValues = new ContentValues(); - mTextDataId = 0; - mCallDataId = 0; - } -/* - * 检查是否有本地修改的方法。 - * @return 如果有本地修改,则返回 true;否则返回 false。 - */ - boolean isLocalModified() { - return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; - } -/* - * 设置文本数据的唯一标识符。 - * 该方法用于设置文本数据的唯一标识符。如果传入的 id 小于或等于 0,则抛出 IllegalArgumentException 异常。 - * @param id 文本数据的唯一标识符。 - * @throws IllegalArgumentException 如果 id 小于或等于 0,则抛出此异常。 - */ - void setTextDataId(long id) { - // 检查传入的 id 是否有效,如果无效则抛出异常。 - if(id <= 0) { - throw new IllegalArgumentException("Text data id should larger than 0"); - } - mTextDataId = id;// 设置文本数据的唯一标识符。 - } -/* - * 设置通话数据的唯一标识符。 - * 该方法用于设置通话数据的唯一标识符。如果传入的 id 小于或等于 0,则抛出 IllegalArgumentException 异常。 - * @param id 通话数据的唯一标识符。 - * @throws IllegalArgumentException 如果 id 小于或等于 0,则抛出此异常。 - */ - void setCallDataId(long id) { - // 检查传入的 id 是否有效,如果无效则抛出异常。 - if (id <= 0) { - throw new IllegalArgumentException("Call data id should larger than 0"); - } - mCallDataId = id; // 设置通话数据的唯一标识符。 - } -/* - * 设置通话数据的方法。 - * 该方法用于将给定的键值对存储到通话数据的 ContentValues 对象中,并更新笔记的本地修改状态和修改日期。 - * @param key 通话数据的键。 - * @param value 通话数据的值。 - */ - void setCallData(String key, String value) { - // 将给定的键值对存储到通话数据的 ContentValues 对象中。 - mCallDataValues.put(key, value); - // 更新笔记的本地修改状态为 1,表示有本地修改。 - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - // 更新笔记的修改日期为当前时间。 - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); - } -/* - *设置文本数据的方法。 - * 该方法用于将给定的键值对存储到文本数据的 ContentValues 对象中,并更新笔记的本地修改状态和修改日期。 - * @param key 文本数据的键。 - * @param value 文本数据的值。 - */ - void setTextData(String key, String value) { - // 将给定的键值对存储到文本数据的 ContentValues 对象中。 - mTextDataValues.put(key, value); - // 更新笔记的本地修改状态为 1,表示有本地修改。 - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - // 更新笔记的修改日期为当前时间。 - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); - } -/* - * 将数据推送到内容提供者的方法。 - * 该方法用于将文本数据和通话数据推送到内容提供者中。如果文本数据或通话数据有修改,则将其插入或更新到内容提供者中。 - * @param context 应用程序上下文。 - * @param noteId 笔记的唯一标识符。 - * @return 如果推送成功,则返回 Uri;否则返回 null。 - * @throws IllegalArgumentException 如果 noteId 小于或等于 0,则抛出此异常。 - */ - Uri pushIntoContentResolver(Context context, long noteId) { - // 这是方法的签名,定义了方法的访问修饰符、返回类型、方法名称和参数列表。 - /** - * Check for safety - */ - //检查 noteId 是否有效,如果无效则抛出 - if (noteId <= 0) { - throw new IllegalArgumentException("Wrong note id:" + noteId); - } - //创建一个操作列表,用于批量操作。 - ArrayList operationList = new ArrayList(); - ContentProviderOperation.Builder builder = null; - //如果文本数据有修改,处理文本数据。 - if(mTextDataValues.size() > 0) { - //将笔记 ID 添加到文本数据的 ContentValues 对象中。 - mTextDataValues.put(DataColumns.NOTE_ID, noteId); - //如果文本数据的 ID 为 0,表示是新数据,插入到内容提供者中。 - 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; - } - } //如果文本数据的 ID 不为 0,表示是已有数据,更新到内容提供者中。 - else { - builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mTextDataId)); - builder.withValues(mTextDataValues); - operationList.add(builder.build()); - } - mTextDataValues.clear();// 清空文本数据的 ContentValues 对象。 - } - // 如果通话数据有修改,处理通话数据。 - if(mCallDataValues.size() > 0) { - mCallDataValues.put(DataColumns.NOTE_ID, noteId); - //将笔记 ID 添加到通话数据的 ContentValues 对象中。 - //如果通话数据的 ID 为 0,表示是新数据,插入到内容提供者中。 - 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; - } - } //如果通话数据的 ID 不为 0,表示是已有数据,更新到内容提供者中。 - else { - builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mCallDataId)); - builder.withValues(mCallDataValues); - operationList.add(builder.build()); - } - mCallDataValues.clear();//清空通话数据的 ContentValues 对象。 - } - //如果操作列表中有操作,执行批量操作。 - 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 e) { - //捕获远程异常并记录日志。 - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } catch (OperationApplicationException e) { - // 捕获操作应用异常并记录日志。 - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } - } - return null; - } - } -} -- 2.34.1 From 1549889b3fb618f7b1f149380b6890c5944211bf Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:19:03 +0800 Subject: [PATCH 37/44] Delete 'Notes.java' --- Notes.java | 441 ----------------------------------------------------- 1 file changed, 441 deletions(-) delete mode 100644 Notes.java diff --git a/Notes.java b/Notes.java deleted file mode 100644 index 0d1d179..0000000 --- a/Notes.java +++ /dev/null @@ -1,441 +0,0 @@ -/* - * 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; -/* - * 定义了与笔记相关的常量和接口 - */ -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; //系统文件夹 - - /** - * Following IDs are system folders' identifiers - * {@link Notes#ID_ROOT_FOLDER } is default folder - * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder - * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records - */ - /* - * 系统文件夹的标识符 - * {@link Notes#ID_ROOT_FOLDER }是默认文件夹 - * {@link Notes#ID_TEMPARAY_FOLDER }是用于没有所属文件夹的笔记 - * {@link Notes#ID_CALL_RECORD_FOLDER}是用于存储通话记录的文件夹 - */ - public static final int ID_ROOT_FOLDER = 0; - public static final int ID_TEMPARAY_FOLDER = -1; - public static final int ID_CALL_RECORD_FOLDER = -2; - public static final int ID_TRASH_FOLER = -3; - //用于Intent传递的额外数据键 - public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; - public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; - public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_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"; - 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 to query all notes and folders - */ - /* - * Uri用于查询所有的笔记和文件夹 - */ - public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); - - /** - * Uri to query data - */ - /* - * Uri用于查询数据 - */ - public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); - - /* - * 笔记表的列定义 - */ - public interface NoteColumns { - /** - * The unique ID for a row - *

Type: INTEGER (long)

- */ - /* - * 行的唯一ID - *

类型:INTEGER(long)

- */ - public static final String ID = "_id"; - - /** - * The parent's id for note or folder - *

Type: INTEGER (long)

- */ - /* - * 笔记或文件夹的父ID - *

类型:INTEGER (long)

- */ - public static final String PARENT_ID = "parent_id"; - - /** - * Created data for note or folder - *

Type: INTEGER (long)

- */ - /* - * 创建日期 - *

类型:INTEGER (long)

- */ - public static final String CREATED_DATE = "created_date"; - - /** - * Latest modified date - *

Type: INTEGER (long)

- */ - /* - * 最后修改日期 - *

类型:INTEGER (long)

- */ - public static final String MODIFIED_DATE = "modified_date"; - - - /** - * Alert date - *

Type: INTEGER (long)

- */ - /* - * 提醒日期 - *

类型:INTEGER (long)

- */ - public static final String ALERTED_DATE = "alert_date"; - - /** - * Folder's name or text content of note - *

Type: TEXT

- */ - /* - * 文件夹的名称或笔记的文本内容 - *

类型:TEXT

- */ - public static final String SNIPPET = "snippet"; - - /** - * Note's widget id - *

Type: INTEGER (long)

- */ - /* - * 笔记的小部件ID - *

类型:INTEGER (long)

- */ - public static final String WIDGET_ID = "widget_id"; - - /** - * Note's widget type - *

Type: INTEGER (long)

- */ - /* - * 笔记的小部件类型 - *

类型:INTEGER (long)

- */ - public static final String WIDGET_TYPE = "widget_type"; - - /** - * Note's background color's id - *

Type: INTEGER (long)

- */ - /* - * 笔记的背景颜色ID - *

类型:INTEGER (long)

- */ - public static final String BG_COLOR_ID = "bg_color_id"; - - /** - * For text note, it doesn't has attachment, for multi-media - * note, it has at least one attachment - *

Type: INTEGER

- */ - /* - * 是否包含组件 - *

类型:INTEGER

- */ - public static final String HAS_ATTACHMENT = "has_attachment"; - - /** - * Folder's count of notes - *

Type: INTEGER (long)

- */ - /* - * 文件夹中的笔记数量 - *

类型:INTEGER (long)

- */ - public static final String NOTES_COUNT = "notes_count"; - - /** - * The file type: folder or note - *

Type: INTEGER

- */ - /* - * 文件类型:文件夹或笔记 - *

类型:INTEGER

- */ - public static final String TYPE = "type"; - - /** - * The last sync id - *

Type: INTEGER (long)

- */ - /* - * 最后一次同步的ID - *

类型:INTEGER (long)

- */ - public static final String SYNC_ID = "sync_id"; - - /** - * Sign to indicate local modified or not - *

Type: INTEGER

- */ - /* - * 指示是否在本地被修改 - *

类型:INTEGER

- */ - public static final String LOCAL_MODIFIED = "local_modified"; - - /** - * Original parent id before moving into temporary folder - *

Type : INTEGER

- */ - /* - * 移动到临时文件夹之前的原始父ID - *

类型:INTEGER

- */ - public static final String ORIGIN_PARENT_ID = "origin_parent_id"; - - /** - * The gtask id - *

Type : TEXT

- */ - /* - * gtask ID - *

类型:TEXT

- */ - public static final String GTASK_ID = "gtask_id"; - - /** - * The version code - *

Type : INTEGER (long)

- */ - /* - * 版本号 - *

类型:INTEGER (long)

- */ - public static final String VERSION = "version"; - } - /* - * 数据表的列定义 - */ - public interface DataColumns { - /** - * The unique ID for a row - *

Type: INTEGER (long)

- */ - /* - * 行的唯一ID - *

类型:INTEGER (long)

- */ - public static final String ID = "_id"; - - /** - * The MIME type of the item represented by this row. - *

Type: Text

- */ - /* - * 该行的MIME类型 - *

类型:TEXT

- */ - public static final String MIME_TYPE = "mime_type"; - - /** - * The reference id to note that this data belongs to - *

Type: INTEGER (long)

- */ - /* - * 该数据所属的笔记ID - *

类型:INTEGER (long)

- */ - public static final String NOTE_ID = "note_id"; - - /** - * Created data for note or folder - *

Type: INTEGER (long)

- */ - /* - * 创建日期 - *

类型:INTEGER (long)

- */ - public static final String CREATED_DATE = "created_date"; - - /** - * Latest modified date - *

Type: INTEGER (long)

- */ - /* - * 最后修改日期 - *

类型:INTEGER (long)

- */ - public static final String MODIFIED_DATE = "modified_date"; - - /** - * Data's content - *

Type: TEXT

- */ - /* - * 数据内容 - *

类型:TEXT

- */ - public static final String CONTENT = "content"; - - - /** - * Generic data column, the meaning is{@link #MIMETYPE} specific, used for - * integer data type - *

Type: INTEGER

- */ - /* - * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于整数数据类型 - *

类型:INTEGER

- */ - public static final String DATA1 = "data1"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * integer data type - *

Type: INTEGER

- */ - /* - * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于整数数据类型 - *

类型: INTEGER

- */ - public static final String DATA2 = "data2"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * TEXT data type - *

Type: TEXT

- */ - /* - * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于TEXT数据类型 - *

类型: TEXT

- */ - public static final String DATA3 = "data3"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * TEXT data type - *

Type: TEXT

- */ - /* - * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于TEXT数据类型 - *

类型: TEXT

- */ - public static final String DATA4 = "data4"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * TEXT data type - *

Type: TEXT

- */ - /* - * 通用数据列,具体含义由{@link #MIMETYPE}决定,用于TEXT数据类型 - *

类型: TEXT

- */ - public static final String DATA5 = "data5"; - } - /* - * 文本笔记的定义 - */ - public static final class TextNote implements DataColumns { - /** - * Mode to indicate the text in check list mode or not - *

Type: Integer 1:check list mode 0: normal mode

- */ - /* - * 模式,指示文本是否在检查列表模式 - *

类型: 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"; - - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; - - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); - } - /* - * 通话笔记的定义 - */ - public static final class CallNote implements DataColumns { - /** - * Call date for this record - *

Type: INTEGER (long)

- */ - /* - * 通话日期 - *

类型: INTEGER (long)

- */ - public static final String CALL_DATE = DATA1; - - /** - * Phone number for this record - *

Type: TEXT

- */ - /* - * 电话号码 - *

类型: TEXT

- */ - - public static final String PHONE_NUMBER = DATA3; - - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; - - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; - - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); - } -} - -/* - * 版权声明:保留了原有的版权声明,说明代码的许可协议和版权信息。 - - 常量定义:解释了各个常量的用途,如笔记类型、系统文件夹ID、Intent传递的额外数据键等。 - - 接口定义:详细说明了各个接口中的列定义,解释了每个列的用途和数据类型。 - - 类定义:解释了 TextNote 和 CallNote 类的用途,并详细说明了它们的数据列和常量。 - */ -- 2.34.1 From adf00f20ef9c228046b5e815a1eb9000463073c0 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:19:13 +0800 Subject: [PATCH 38/44] Delete 'NotesDatabaseHelper.java' --- NotesDatabaseHelper.java | 475 --------------------------------------- 1 file changed, 475 deletions(-) delete mode 100644 NotesDatabaseHelper.java diff --git a/NotesDatabaseHelper.java b/NotesDatabaseHelper.java deleted file mode 100644 index 3281922..0000000 --- a/NotesDatabaseHelper.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * 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.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; - -/* - * NotesDatabaseHelper 类继承自 SQLiteOpenHelper,用于管理 SQLite 数据库的创建和版本升级。 - * 该类负责创建和管理两个主要的数据库表:NOTE 表和 DATA 表,以及相关的触发器和索引。 - */ -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 语句 - /* - * 创建笔记表的 SQL 语句。 - * 该表用于存储笔记的相关信息,包括笔记的 ID、父 ID、提醒日期、背景颜色 ID、创建日期、 - * 是否有附件、修改日期、笔记数量、摘要、类型、小部件 ID、小部件类型、同步 ID、本地修改标志、 - * 原始父 ID、Google 任务 ID 和版本号。 - */ - 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," + // 父笔记的 ID,默认为 0 - NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 提醒日期,默认为 0 - NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景颜色 ID,默认为 0 - NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建日期,默认为当前时间 - NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + // 是否有附件,默认为 0 - NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改日期,默认为当前时间 - NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 笔记数量,默认为 0 - NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 笔记摘要,默认为空字符串 - NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 笔记类型,默认为 0 - NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + // 小部件 ID,默认为 0 - NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 小部件类型,默认为 -1 - NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步 ID,默认为 0 - NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 本地修改标志,默认为 0 - NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 原始父 ID,默认为 0 - NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // Google 任务 ID,默认为空字符串 - NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本号,默认为 0 - ")"; - // 创建 DATA 表的 SQL 语句 - /* - * 创建数据表的 SQL 语句。 - * 该表用于存储与笔记相关的数据,包括数据的 ID、MIME 类型、所属笔记的 ID、创建日期、 - * 修改日期、内容、以及一些额外的数据字段(DATA1 到 DATA5)。 - */ - private static final String CREATE_DATA_TABLE_SQL = - "CREATE TABLE " + TABLE.DATA + "(" + - DataColumns.ID + " INTEGER PRIMARY KEY," + // 数据的唯一标识符,主键 - DataColumns.MIME_TYPE + " TEXT NOT NULL," + // MIME 类型,不能为空 - DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 所属笔记的 ID,默认为 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," + // 额外数据字段 1,整数类型 - DataColumns.DATA2 + " INTEGER," + // 额外数据字段 2,整数类型 - DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 额外数据字段 3,文本类型,默认为空字符串 - DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 额外数据字段 4,文本类型,默认为空字符串 - DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 额外数据字段 5,文本类型,默认为空字符串 - ")"; - // 创建 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 + ");"; - // 当笔记移动到文件夹时,增加文件夹的笔记计数触发器 - /** - * Increase folder's note count when move note to the folder - */ - /* - * 创建一个触发器,用于在更新笔记的父 ID 时增加父文件夹的笔记数量。 - * 该触发器在笔记表的 PARENT_ID 字段更新后触发,并将父文件夹的 NOTES_COUNT 字段加 1。 - */ - 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"; - // 当笔记从文件夹中移出时,减少文件夹的笔记计数触发器 - /** - * Decrease folder's note count when move note from folder - */ - /* - * 创建一个触发器,用于在更新笔记的父 ID 时减少旧父文件夹的笔记数量。 - * 该触发器在笔记表的 PARENT_ID 字段更新后触发,并将旧父文件夹的 NOTES_COUNT 字段减 1。 - */ - 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"; - // 当插入新笔记到文件夹时,增加文件夹的笔记计数触发器 - /** - * Increase folder's note count when insert new note to the folder - */ - /* - * 创建一个触发器,用于在插入新笔记时增加父文件夹的笔记数量。 - * 该触发器在笔记表插入新记录后触发,并将父文件夹的 NOTES_COUNT 字段加 1。 - */ - 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"; - // 当从文件夹中删除笔记时,减少文件夹的笔记计数触发器 - /** - * Decrease folder's note count when delete note from the folder - */ - /* - * 创建一个触发器,用于在删除笔记时减少父文件夹的笔记数量。 - * 该触发器在笔记表删除记录后触发,并将父文件夹的 NOTES_COUNT 字段减 1。 - */ - 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"; - // 当插入类型为 {@link DataConstants#NOTE} 的数据时,更新笔记内容触发器 - /** - * Update note's content when insert data with type {@link DataConstants#NOTE} - */ - /* - * 创建一个触发器,用于在插入新数据时更新笔记的内容摘要。 - * 该触发器在数据表插入新记录后触发,并且仅在插入的数据 MIME 类型为 'NOTE' 时执行。 - * 触发器会将笔记表中对应笔记的 SNIPPET 字段更新为新插入数据的内容。 - */ - 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"; - // 当类型为 {@link DataConstants#NOTE} 的数据更新时,更新笔记内容触发器 - /** - * Update note's content when data with {@link DataConstants#NOTE} type has changed - */ - /* - * 创建一个触发器,用于在更新数据时更新笔记的内容摘要。 - * 该触发器在数据表更新记录后触发,并且仅在更新的数据 MIME 类型为 'NOTE' 时执行。 - * 触发器会将笔记表中对应笔记的 SNIPPET 字段更新为更新后的数据内容。 - */ - 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"; - // 当类型为 {@link DataConstants#NOTE} 的数据删除时,更新笔记内容触发器 - /** - * Update note's content when data with {@link DataConstants#NOTE} type has deleted - */ - /* - *创建一个触发器,用于在删除数据时更新笔记的内容摘要。 - * 该触发器在数据表删除记录后触发,并且仅在删除的数据 MIME 类型为 'NOTE' 时执行。 - * 触发器会将笔记表中对应笔记的 SNIPPET 字段清空。 - */ - 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"; - // 当删除笔记时,删除相关数据触发器 - /** - * Delete datas belong to note which has been deleted - */ - /* - * 创建一个触发器,用于在删除笔记时删除与之关联的数据。 - * 该触发器在笔记表删除记录后触发,并将数据表中与被删除笔记关联的所有数据删除。 - */ - 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"; - // 当删除文件夹时,删除文件夹中的笔记触发器 - /** - * Delete notes belong to folder which has been deleted - */ - /* - *创建一个触发器,用于在删除笔记时删除与之关联的数据。 - * 该触发器在笔记表删除记录后触发,并将数据表中与被删除笔记关联的所有数据删除。 - */ - 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"; - // 当文件夹移动到回收站时,移动文件夹中的笔记到回收站触发器 - /** - * Move notes belong to folder which has been moved to trash folder - */ - /* - * 创建一个触发器,用于在将文件夹移动到回收站时,将其所有子笔记也移动到回收站。 - * 该触发器在笔记表更新记录后触发,并且仅在更新的笔记的 PARENT_ID 字段值为回收站文件夹 ID 时执行。 - * 触发器会将所有父 ID 为被更新笔记 ID 的笔记的 PARENT_ID 字段值更新为回收站文件夹 ID。 - */ - 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"; -/* - * 构造函数,初始化数据库 - * 该构造函数用于初始化数据库帮助类,并指定数据库名称和版本号。 - * @param context 上下文 - */ - public NotesDatabaseHelper(Context context) { - super(context, DB_NAME, null, DB_VERSION); - } -/* - * 创建 NOTE 表 - * 创建笔记表及其相关触发器和系统文件夹。 - * 该方法用于在数据库中创建笔记表,并调用相关方法创建触发器和系统文件夹。 - * @param db SQLiteDatabase 实例 - */ - public void createNoteTable(SQLiteDatabase db) { - db.execSQL(CREATE_NOTE_TABLE_SQL); // 执行创建笔记表的 SQL 语句 - reCreateNoteTableTriggers(db); // 重新创建笔记表的触发器 - createSystemFolder(db); // 创建系统文件夹 - Log.d(TAG, "note table has been created"); // 记录日志,表示笔记表已创建 - } -/* - * 重新创建 NOTE 表的触发器 - * 该方法用于删除现有的笔记表触发器,并重新创建它们,以确保触发器与表结构一致。 - * @param 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); - } -/* - * 创建系统文件夹 - * 该方法用于在数据库中创建系统文件夹,包括通话记录文件夹、根文件夹、临时文件夹和回收站文件夹。 - * @param db SQLiteDatabase 实例 - */ - private void createSystemFolder(SQLiteDatabase db) { - ContentValues values = new ContentValues(); - // 创建通话记录文件夹, 通话记录文件夹,用于存储通话笔记。 - /** - * call record foler for call notes - */ - values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - // 创建根文件夹(默认文件夹) - /** - * root folder which is default folder - */ - values.clear(); - values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - // 创建临时文件夹(用于移动笔记) - /** - * temporary folder which is used for moving note - */ - values.clear(); - values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - // 创建回收站文件夹 - /** - * create trash folder - */ - values.clear(); - values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - } -/* - * 创建 DATA 表 - * 该方法用于在数据库中创建数据表,并调用相关方法创建触发器和索引。 - * @param db SQLiteDatabase 实例 - */ - public void createDataTable(SQLiteDatabase db) { - db.execSQL(CREATE_DATA_TABLE_SQL); // 执行创建数据表的 SQL 语句 - reCreateDataTableTriggers(db); // 重新创建数据表的触发器 - db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); // 创建数据表的索引 - Log.d(TAG, "data table has been created"); // 记录日志,表示数据表已创建 - } -/* - * 重新创建 DATA 表的触发器 - * 该方法用于删除现有的数据表触发器,并重新创建它们,以确保触发器与表结构一致。 - * @param 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 的单例实例 - * 该方法使用 synchronized 关键字确保线程安全,并在实例不存在时创建一个新的实例。 - * @param context 上下文 - * @return 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 旧版本号 - * @param newVersion 新版本号 - */ - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - boolean reCreateTriggers = false; - boolean skipV2 = false; - // 如果旧版本是 1,执行从版本 1 升级到版本 2 的操作 - if (oldVersion == 1) { - upgradeToV2(db); - skipV2 = true; // // 这个升级包括从 v2 到 v3 的升级 - oldVersion++; - } - // 如果旧版本是 2,并且没有跳过 v2 升级,执行从版本 2 升级到版本 3 的操作 - if (oldVersion == 2 && !skipV2) { - upgradeToV3(db); - reCreateTriggers = true; - oldVersion++; - } - // 如果旧版本是 3,执行从版本 3 升级到版本 4 的操作 - 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"); - } - } -/* - * 升级数据库到版本 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); // 重新创建数据表及其相关触发器和索引 - } -/* - * 升级数据库到版本 3 - * @param db SQLiteDatabase 实例 - */ - private void upgradeToV3(SQLiteDatabase db) { - // drop unused triggers - // 删除不再使用的触发器 - 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"); - // add a column for gtask id - //为 gtask id 添加列 - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID - + " TEXT NOT NULL DEFAULT ''"); - // add a trash system folder - // 添加回收站系统文件夹 - 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); - } -/* - *升级数据库到版本 4 - * @param db SQLiteDatabase 实例 - */ - private void upgradeToV4(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION - + " INTEGER NOT NULL DEFAULT 0"); - } -} -- 2.34.1 From f7924c097e7cfb9599abdd419ad8a244953ecfda Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:19:22 +0800 Subject: [PATCH 39/44] Delete 'NotesProvider.java' --- NotesProvider.java | 375 --------------------------------------------- 1 file changed, 375 deletions(-) delete mode 100644 NotesProvider.java diff --git a/NotesProvider.java b/NotesProvider.java deleted file mode 100644 index 0bff3b3..0000000 --- a/NotesProvider.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * 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.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; - -/* - * NotesProvider 是一个 ContentProvider,用于管理笔记数据的增删改查操作。 - * 它通过 UriMatcher 来匹配不同的 URI,并根据匹配结果执行相应的数据库操作。 - */ -public class NotesProvider extends ContentProvider { - private static final UriMatcher mMatcher; - // UriMatcher 用于匹配不同的 URI - 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; // 匹配搜索建议 - // URI 匹配码 - static { // 初始化 UriMatcher - 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); - } -/* - * 搜索结果的投影字段,用于构建搜索查询。 - *x'0A' 表示 SQLite 中的换行符 '\n'。 - * 在搜索结果中,标题和内容会去除换行符和空白字符,以便显示更多信息。 - */ - /** - * x'0A' represents the '\n' character in sqlite. For title and content in the search result, - * we will trim '\n' and white space in order to show more information. - */ - 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 查询语句。 - * 查询条件包括:笔记摘要包含搜索关键字、父 ID 不是回收站文件夹、笔记类型为普通笔记。 - */ - 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; // 返回 true 表示初始化成功 - } -/* - * 查询操作。 - * 该方法根据传入的 URI 执行相应的查询操作,并返回查询结果的 Cursor。 - * @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; - 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); // 获取 URI 中的笔记 ID - 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); // 获取 URI 中的数据 ID - c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id - + parseSelection(selection), selectionArgs, null, null, sortOrder); // 查询单个数据项 - break; - case URI_SEARCH: - case URI_SEARCH_SUGGEST: - if (sortOrder != null || projection != null) { - throw new IllegalArgumentException( - "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); - } - - String searchString = null; - if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { - if (uri.getPathSegments().size() > 1) { - searchString = uri.getPathSegments().get(1); // 获取搜索建议的搜索字符串 - } - } else { - searchString = uri.getQueryParameter("pattern"); // 获取搜索的搜索字符串 - } - - 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); // 设置 Cursor 的通知 URI - } - return c; // 返回查询结果的 Cursor - } -/* - * 插入操作。 - * 该方法根据传入的 URI 执行相应的插入操作,并返回插入后的 URI。 - * @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); // 获取笔记 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 异常 - } - // Notify the note uri - // 通知笔记 URI 变化 - if (noteId > 0) { - getContext().getContentResolver().notifyChange( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); - } - - // Notify the data uri - // 通知数据 URI 变化 - if (dataId > 0) { - getContext().getContentResolver().notifyChange( - ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); - } - - return ContentUris.withAppendedId(uri, insertedId); // 返回插入后的 URI - } -/* - * 删除操作。 - * 该方法根据传入的 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 "; // 添加删除条件,ID 大于 0 - count = db.delete(TABLE.NOTE, selection, selectionArgs); // 删除笔记表中的数据 - break; - case URI_NOTE_ITEM: - id = uri.getPathSegments().get(1); // 获取 URI 中的笔记 ID - /** - * ID that smaller than 0 is system folder which is not allowed to - * trash - */ - /* - * 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); // 获取 URI 中的数据 ID - 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); // 通知笔记 URI 变化 - } - getContext().getContentResolver().notifyChange(uri, null); // 通知 URI 变化 - } - return count; // 返回删除的行数 - } -/* - * 更新操作。 - * 该方法根据传入的 URI 执行相应的更新操作,并返回更新的行数。 - * @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); // 获取 URI 中的笔记 ID - 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); // 获取 URI 中的数据 ID - 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); // 通知笔记 URI 变化 - } - getContext().getContentResolver().notifyChange(uri, null); // 通知 URI 变化 - } - return count; // 返回更新的行数 - } -/* - * 解析选择条件,用于在查询或更新时附加额外的条件。 - * @param selection 原始选择条件。 - * @return 返回附加了额外条件的字符串。 - */ - private String parseSelection(String selection) { - return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); - } -/* - * 增加笔记版本号。 - *该方法用于增加指定笔记的版本号,或者根据选择条件增加符合条件的笔记的版本号。 - * @param id 笔记 ID,如果为 -1 则表示更新所有符合条件的笔记。 - * @param selection 更新条件。 - * @param selectionArgs 更新条件的参数。 - */ - private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { - StringBuilder sql = new StringBuilder(120); // 创建 StringBuilder 对象,用于构建 SQL 语句 - sql.append("UPDATE "); - sql.append(TABLE.NOTE); - sql.append(" SET "); - sql.append(NoteColumns.VERSION); - sql.append("=" + NoteColumns.VERSION + "+1 "); // 增加版本号 - - if (id > 0 || !TextUtils.isEmpty(selection)) { - sql.append(" WHERE "); - } - if (id > 0) { - sql.append(NoteColumns.ID + "=" + String.valueOf(id)); // 根据笔记 ID 更新 - } - if (!TextUtils.isEmpty(selection)) { - String selectString = id > 0 ? parseSelection(selection) : selection; - for (String args : selectionArgs) { - selectString = selectString.replaceFirst("\\?", args); // 替换选择条件中的占位符 - } - sql.append(selectString); - } - - mHelper.getWritableDatabase().execSQL(sql.toString()); // 执行 SQL 语句 - } -/* - * 获取 MIME 类型。 - * @param uri 查询的 URI。 - * @return 返回 MIME 类型。 - */ - @Override - public String getType(Uri uri) { - // TODO Auto-generated method stub - return null; - } - -} -- 2.34.1 From 4d954734ab2f57cc85dc1ce6815d0f9a19447e51 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:19:31 +0800 Subject: [PATCH 40/44] Delete 'SqlData.java' --- SqlData.java | 243 --------------------------------------------------- 1 file changed, 243 deletions(-) delete mode 100644 SqlData.java diff --git a/SqlData.java b/SqlData.java deleted file mode 100644 index 4cdeb85..0000000 --- a/SqlData.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.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; - -/* - * SqlData 类用于处理与数据库相关的数据操作。 - * 该类主要用于加载、设置、获取和提交数据。 - */ -public class SqlData { - private static final String TAG = SqlData.class.getSimpleName(); - - private static final int INVALID_ID = -99999; // 无效的 ID - // 数据表的投影字段 - public static final String[] PROJECTION_DATA = new String[] { - DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, - DataColumns.DATA3 - }; - // 数据表的列索引 - 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; - - private ContentResolver mContentResolver; // 内容解析器 - - private boolean mIsCreate; // 是否为创建操作 - - private long mDataId; // 数据 ID - - private String mDataMimeType; // 数据 MIME 类型 - - private String mDataContent; // 数据内容 - - private long mDataContentData1; // 数据内容数据 1 - - private String mDataContentData3; // 数据内容数据 3 - - private ContentValues mDiffDataValues; // 差异数据值 -/* - * 构造函数,用于创建新的数据对象。 - * 该构造函数初始化数据对象的各个属性,并设置为创建操作。 - * @param context 应用程序上下文。 - */ - 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(); // 初始化差异数据值 - } -/* - * 构造函数,用于从数据库游标加载数据对象。 - * 该构造函数初始化数据对象的各个属性,并设置为非创建操作。 - * @param context 应用程序上下文。 - * @param c 数据库游标。 - */ - public SqlData(Context context, Cursor c) { - mContentResolver = context.getContentResolver(); // 获取内容解析器 - mIsCreate = false; // 设置为非创建操作 - loadFromCursor(c); // 从数据库游标加载数据 - mDiffDataValues = new ContentValues(); // 初始化差异数据值 - } -/* - * 从数据库游标加载数据。 - * 该方法从数据库游标中提取数据,并初始化数据对象的各个属性。 - * @param c 数据库游标。 - */ - 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 对象中提取数据,并更新数据对象的各个属性。 - * 如果数据对象是新创建的,或者 JSON 对象中的数据与当前数据对象的数据不同,则更新差异数据值。 - * @param js JSON 对象,包含数据内容。 - * @throws JSONException 如果解析 JSON 对象时发生异常。 - */ - public void setContent(JSONObject js) throws JSONException { - // 从 JSON 对象中获取数据 ID,如果 JSON 对象中没有数据 ID,则使用无效 ID - long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; - // 如果数据对象是新创建的,或者数据 ID 不同,则更新差异数据值 - if (mIsCreate || mDataId != dataId) { - mDiffDataValues.put(DataColumns.ID, dataId); - } - mDataId = dataId; // 更新数据 ID - // 从 JSON 对象中获取数据 MIME 类型,如果 JSON 对象中没有数据 MIME 类型,则使用默认的笔记类型 - 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); - } - mDataMimeType = dataMimeType; // 更新数据 MIME 类型 - // 从 JSON 对象中获取数据内容,如果 JSON 对象中没有数据内容,则使用空字符串 - String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; - // 如果数据对象是新创建的,或者数据内容不同,则更新差异数据值 - if (mIsCreate || !mDataContent.equals(dataContent)) { - mDiffDataValues.put(DataColumns.CONTENT, dataContent); - } - mDataContent = dataContent; // 更新数据内容 - // 从 JSON 对象中获取数据内容数据 1,如果 JSON 对象中没有数据内容数据 1,则使用 0 - long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; - // 如果数据对象是新创建的,或者数据内容数据 1 不同,则更新差异数据值 - if (mIsCreate || mDataContentData1 != dataContentData1) { - mDiffDataValues.put(DataColumns.DATA1, dataContentData1); - } - mDataContentData1 = dataContentData1; // 更新数据内容数据 1 - // 从 JSON 对象中获取数据内容数据 3,如果 JSON 对象中没有数据内容数据 3,则使用空字符串 - String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; - // 如果数据对象是新创建的,或者数据内容数据 3 不同,则更新差异数据值 - if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { - mDiffDataValues.put(DataColumns.DATA3, dataContentData3); - } - mDataContentData3 = dataContentData3; // 更新数据内容数据 3 - } -/* - * 获取数据内容。 - * 该方法将数据对象的各个属性转换为 JSON 对象,并返回该 JSON 对象。 - * @return 返回包含数据内容的 JSON 对象。 - * @throws JSONException 如果生成 JSON 对象时发生异常。 - */ - public JSONObject getContent() throws JSONException { - // 如果数据对象是新创建的,则记录错误日志并返回 null - if (mIsCreate) { - Log.e(TAG, "it seems that we haven't created this in database yet"); - return null; - } - JSONObject js = new JSONObject(); // 创建一个新的 JSON 对象 - js.put(DataColumns.ID, mDataId); // 将数据 ID 添加到 JSON 对象中 - js.put(DataColumns.MIME_TYPE, mDataMimeType); // 将数据 MIME 类型添加到 JSON 对象中 - js.put(DataColumns.CONTENT, mDataContent); // 将数据内容添加到 JSON 对象中 - js.put(DataColumns.DATA1, mDataContentData1); // 将数据内容数据 1 添加到 JSON 对象中 - js.put(DataColumns.DATA3, mDataContentData3); // 将数据内容数据 3 添加到 JSON 对象中 - return js; // 返回 JSON 对象 - } -/* - * 提交数据。 - * 该方法根据数据对象的状态(创建或更新)执行相应的数据库操作。 - * 如果是创建操作,则插入数据到数据库中;如果是更新操作,则更新数据库中的数据。 - * @param noteId 笔记 ID。 - * @param validateVersion 是否验证版本。 - * @param version 版本号。 - */ - public void commit(long noteId, boolean validateVersion, long version) { - // 如果是创建操作 - if (mIsCreate) { - // 如果数据 ID 是无效的,并且差异数据值中包含数据 ID,则移除数据 ID - if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { - mDiffDataValues.remove(DataColumns.ID); - } - // 将笔记 ID 添加到差异数据值中 - mDiffDataValues.put(DataColumns.NOTE_ID, noteId); - // 插入数据到数据库中 - Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); - try { - // 获取插入数据后的数据 ID - mDataId = Long.valueOf(uri.getPathSegments().get(1)); - } catch (NumberFormatException e) { - // 记录错误日志,并抛出操作失败异常 - Log.e(TAG, "Get note id error :" + e.toString()); - throw new ActionFailureException("create note failed"); - } - } 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。 - * 该方法返回数据对象的数据 ID。 - * @return 返回数据 ID。 - */ - public long getId() { - return mDataId; - } -} -- 2.34.1 From 39b2e25c0f7250c33bad6e940167b243a4092683 Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:19:40 +0800 Subject: [PATCH 41/44] Delete 'SqlNote.java' --- SqlNote.java | 618 --------------------------------------------------- 1 file changed, 618 deletions(-) delete mode 100644 SqlNote.java diff --git a/SqlNote.java b/SqlNote.java deleted file mode 100644 index 0283258..0000000 --- a/SqlNote.java +++ /dev/null @@ -1,618 +0,0 @@ -/* - * 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(); - - private static final int INVALID_ID = -99999; // 无效的 ID - // 笔记表的投影字段 - 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; // 数据列表 -/* - * 构造函数,用于创建新的笔记对象。 - * 该构造函数初始化笔记对象的各个属性,并设置为创建操作。 - * @param context 应用程序上下文。 - */ - public SqlNote(Context context) { - mContext = context; // 设置应用程序上下文 - mContentResolver = context.getContentResolver(); // 获取内容解析器 - mIsCreate = true; // 设置为创建操作 - mId = INVALID_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 为无效值 - mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 初始化小部件类型为无效值 - mOriginParent = 0; // 初始化原始父笔记 ID 为 0 - mVersion = 0; // 初始化版本号为 0 - mDiffNoteValues = new ContentValues(); // 初始化差异笔记值 - mDataList = new ArrayList(); // 初始化数据列表 - } -/* - * 构造函数,用于从数据库游标加载笔记对象。 - * 该构造函数初始化笔记对象的各个属性,并设置为非创建操作。 - * @param context 应用程序上下文。 - * @param c 数据库游标。 - */ - public SqlNote(Context context, Cursor c) { - mContext = context; // 设置应用程序上下文 - mContentResolver = context.getContentResolver(); // 获取内容解析器 - mIsCreate = false; // 设置为非创建操作 - loadFromCursor(c); // 从数据库游标加载笔记对象 - mDataList = new ArrayList(); // 初始化数据列表 - if (mType == Notes.TYPE_NOTE) // 如果笔记类型为普通笔记 - loadDataContent(); // 加载笔记的数据内容 - mDiffNoteValues = new ContentValues(); // 初始化差异笔记值 - } -/* - * 构造函数,用于从数据库中加载指定 ID 的笔记对象。 - * 该构造函数初始化笔记对象的各个属性,并设置为非创建操作。 - * @param context 应用程序上下文。 - * @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(); // 初始化差异笔记值 - - } -/* - * 从数据库中加载指定 ID 的笔记对象。 - * 该方法从数据库中查询指定 ID 的笔记数据,并调用 `loadFromCursor(Cursor c)` 方法加载笔记对象。 - * @param id 笔记 ID。 - */ - 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) { - c.moveToNext(); // 移动到游标的第一行 - loadFromCursor(c); // 加载笔记对象 - } else { - Log.w(TAG, "loadFromCursor: cursor = null"); // 记录警告日志,游标为空 - } - } finally { - if (c != null) - c.close(); // 关闭游标 - } - } -/* - * 从数据库游标加载笔记对象。 - * 该方法从数据库游标中提取笔记数据,并初始化笔记对象的各个属性。 - * @param c 数据库游标。 - */ - 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); // 获取版本号 - } -/* - * 加载笔记的数据内容。 - * 该方法从数据库中查询与笔记相关的数据内容,并将其加载到数据列表中。 - */ - private void loadDataContent() { - Cursor c = null; - mDataList.clear(); // 清空数据列表 - try { - // 查询与笔记相关的数据内容 - c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, - "(note_id=?)", new String[] { - String.valueOf(mId) - }, null); - if (c != null) { - if (c.getCount() == 0) { - Log.w(TAG, "it seems that the note has not data"); // 记录警告日志,笔记没有数据内容 - return; - } - while (c.moveToNext()) { - SqlData data = new SqlData(mContext, c); // 从游标中加载数据 - mDataList.add(data); // 将数据内容添加到数据列表中 - } - } else { - 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 { - // 从JSONObject中获取笔记的元数据 - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - // 检查笔记的类型 - if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { - // 如果是系统文件夹,则不能设置内容,记录警告日志 - Log.w(TAG, "cannot set system folder"); - } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { - // 如果是文件夹类型,则只能更新摘要和类型 - // for folder we can only update the snnipet and type - // 获取摘要,如果未提供则默认为空字符串 - String snippet = note.has(NoteColumns.SNIPPET) ? note - .getString(NoteColumns.SNIPPET) : ""; - // 如果是新建笔记或摘要有变化,则更新摘要 - if (mIsCreate || !mSnippet.equals(snippet)) { - mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); - } - mSnippet = snippet; - // 获取类型,如果未提供则默认为普通笔记类型 - int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) - : Notes.TYPE_NOTE; - // 如果是新建笔记或类型有变化,则更新类型 - 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); - // 获取笔记ID,如果未提供则默认为无效ID - long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; - // 如果是新建笔记或ID有变化,则更新ID - if (mIsCreate || mId != id) { - mDiffNoteValues.put(NoteColumns.ID, id); - } - mId = id; - // 获取提醒日期,如果未提供则默认为0 - long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note - .getLong(NoteColumns.ALERTED_DATE) : 0; - // 如果是新建笔记或提醒日期有变化,则更新提醒日期 - if (mIsCreate || mAlertDate != alertDate) { - mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); - } - mAlertDate = alertDate; - // 获取背景颜色ID,如果未提供则默认为默认背景颜色ID - int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note - .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); - // 如果是新建笔记或背景颜色ID有变化,则更新背景颜色ID - if (mIsCreate || mBgColorId != bgColorId) { - mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); - } - mBgColorId = bgColorId; - // 获取创建日期,如果未提供则默认为当前时间 - long createDate = note.has(NoteColumns.CREATED_DATE) ? note - .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); - // 如果是新建笔记或创建日期有变化,则更新创建日期 - if (mIsCreate || mCreatedDate != createDate) { - mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); - } - mCreatedDate = createDate; - // 获取是否有附件,如果未提供则默认为0(无附件) - int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note - .getInt(NoteColumns.HAS_ATTACHMENT) : 0; - // 如果是新建笔记或是否有附件有变化,则更新是否有附件 - if (mIsCreate || mHasAttachment != hasAttachment) { - mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); - } - mHasAttachment = hasAttachment; - // 获取修改日期,如果未提供则默认为当前时间 - long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note - .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); - // 如果是新建笔记或修改日期有变化,则更新修改日期 - if (mIsCreate || mModifiedDate != modifiedDate) { - mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); - } - mModifiedDate = modifiedDate; - // 获取父ID,如果未提供则默认为0 - long parentId = note.has(NoteColumns.PARENT_ID) ? note - .getLong(NoteColumns.PARENT_ID) : 0; - // 如果是新建笔记或父ID有变化,则更新父ID - if (mIsCreate || mParentId != parentId) { - mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); - } - mParentId = parentId; - // 获取摘要,如果未提供则默认为空字符串 - String snippet = note.has(NoteColumns.SNIPPET) ? note - .getString(NoteColumns.SNIPPET) : ""; - // 如果是新建笔记或摘要有变化,则更新摘要 - if (mIsCreate || !mSnippet.equals(snippet)) { - mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); - } - mSnippet = snippet; - // 获取类型,如果未提供则默认为普通笔记类型 - int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) - : Notes.TYPE_NOTE; - // 如果是新建笔记或类型有变化,则更新类型 - if (mIsCreate || mType != type) { - mDiffNoteValues.put(NoteColumns.TYPE, type); - } - mType = type; - // 获取小部件ID,如果未提供则默认为无效小部件ID - int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) - : AppWidgetManager.INVALID_APPWIDGET_ID; - // 如果是新建笔记或小部件ID有变化,则更新小部件ID - if (mIsCreate || mWidgetId != widgetId) { - mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); - } - mWidgetId = widgetId; - // 获取小部件类型,如果未提供则默认为无效小部件类型 - int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note - .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; - // 如果是新建笔记或小部件类型有变化,则更新小部件类型 - if (mIsCreate || mWidgetType != widgetType) { - mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); - } - mWidgetType = widgetType; - // 获取原始父ID,如果未提供则默认为0 - long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note - .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; - // 如果是新建笔记或原始父ID有变化,则更新原始父ID - if (mIsCreate || mOriginParent != originParent) { - mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); - } - mOriginParent = originParent; - // 遍历数据数组,更新数据列表 - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - SqlData sqlData = null; - // 如果数据对象包含ID,则在现有数据列表中查找匹配的SqlData对象 - if (data.has(DataColumns.ID)) { - long dataId = data.getLong(DataColumns.ID); - for (SqlData temp : mDataList) { - if (dataId == temp.getId()) { - sqlData = temp; - } - } - } - // 如果未找到匹配的SqlData对象,则创建一个新的SqlData对象并添加到数据列表中 - if (sqlData == null) { - sqlData = new SqlData(mContext); - mDataList.add(sqlData); - } - // 设置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用于存储内容 - JSONObject js = new JSONObject(); - // 如果当前对象尚未在数据库中创建,记录错误日志并返回null - if (mIsCreate) { - Log.e(TAG, "it seems that we haven't created this in database yet"); - return null; - } - // 创建一个新的JSONObject用于存储笔记信息 - JSONObject note = new JSONObject(); - // 如果当前对象是普通笔记类型 - if (mType == Notes.TYPE_NOTE) { - // 将笔记的各个属性添加到note JSONObject中 - note.put(NoteColumns.ID, mId); - note.put(NoteColumns.ALERTED_DATE, mAlertDate); - note.put(NoteColumns.BG_COLOR_ID, mBgColorId); - note.put(NoteColumns.CREATED_DATE, mCreatedDate); - note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); - note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); - note.put(NoteColumns.PARENT_ID, mParentId); - note.put(NoteColumns.SNIPPET, mSnippet); - note.put(NoteColumns.TYPE, mType); - note.put(NoteColumns.WIDGET_ID, mWidgetId); - note.put(NoteColumns.WIDGET_TYPE, mWidgetType); - note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); - // 将note JSONObject添加到js JSONObject中 - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - // 创建一个新的JSONArray用于存储数据列表 - JSONArray dataArray = new JSONArray(); - // 遍历数据列表,将每个SqlData对象的内容添加到dataArray中 - for (SqlData sqlData : mDataList) { - JSONObject data = sqlData.getContent(); - if (data != null) { - dataArray.put(data); - } - } - // 将dataArray添加到js JSONObject中 - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { - // 如果当前对象是文件夹或系统文件夹类型 - // 将笔记的ID、类型和摘要添加到note JSONObject中 - note.put(NoteColumns.ID, mId); - note.put(NoteColumns.TYPE, mType); - note.put(NoteColumns.SNIPPET, mSnippet); - // 将note JSONObject添加到js JSONObject中 - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - } - // 返回包含内容的JSONObject - return js; - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - return null; - } -/* - * 设置父ID,并将其添加到差异值中。 - * @param id 新的父ID - */ - public void setParentId(long id) { - mParentId = id; - mDiffNoteValues.put(NoteColumns.PARENT_ID, id); - } -/* - * 设置Google任务ID,并将其添加到差异值中。 - * @param gid 新的Google任务ID - */ - public void setGtaskId(String gid) { - mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); - } -/* - * 设置同步ID,并将其添加到差异值中。 - * @param syncId 新的同步ID - */ - public void setSyncId(long syncId) { - mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); - } -/* - * 重置本地修改标志,将其设置为0。 - */ - public void resetLocalModified() { - mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); - } -/* - * 获取当前笔记的ID。 - * @return 当前笔记的ID - */ - public long getId() { - return mId; - } -/* - * 获取当前笔记的父ID。 - * @return 当前笔记的父ID - */ - public long getParentId() { - return mParentId; - } -/* - * 获取当前笔记的摘要。 - * @return 当前笔记的摘要 - */ - public String getSnippet() { - return mSnippet; - } -/* - * 判断当前笔记是否为普通笔记类型。 - * @return 如果当前笔记是普通笔记类型,返回true;否则返回false - */ - public boolean isNoteType() { - return mType == Notes.TYPE_NOTE; - } -/* - * 提交更改,将当前对象的差异值保存到数据库中。 - * @param validateVersion 是否验证版本号 - */ - public void commit(boolean validateVersion) { - // 如果是新建笔记 - if (mIsCreate) { - // 如果ID无效且差异值中包含ID,则移除ID - if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { - mDiffNoteValues.remove(NoteColumns.ID); - } - // 插入笔记到数据库,并获取插入后的URI - Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); - try { - // 从URI中提取新插入笔记的ID - mId = Long.valueOf(uri.getPathSegments().get(1)); - } catch (NumberFormatException e) { - // 如果提取ID失败,记录错误日志并抛出异常 - Log.e(TAG, "Get note id error :" + e.toString()); - throw new ActionFailureException("create note failed"); - } - // 如果ID为0,表示插入失败,抛出异常 - if (mId == 0) { - throw new IllegalStateException("Create thread id failed"); - } - // 如果当前笔记是普通笔记类型 - if (mType == Notes.TYPE_NOTE) { - // 遍历数据列表,提交每个SqlData对象的更改 - 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) - }); - } else { - // 如果验证版本号,更新笔记内容并检查版本号 - result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", - new String[] { - String.valueOf(mId), String.valueOf(mVersion) - }); - } - // 如果没有更新内容,记录警告日志 - if (result == 0) { - Log.w(TAG, "there is no update. maybe user updates note when syncing"); - } - } - // 如果当前笔记是普通笔记类型 - if (mType == Notes.TYPE_NOTE) { - // 遍历数据列表,提交每个SqlData对象的更改 - for (SqlData sqlData : mDataList) { - sqlData.commit(mId, validateVersion, mVersion); - } - } - } - - // refresh local info - // 刷新本地信息 - loadFromCursor(mId); - if (mType == Notes.TYPE_NOTE) - loadDataContent(); - // 清空差异值并设置为非新建状态 - mDiffNoteValues.clear(); - mIsCreate = false; - } -} -- 2.34.1 From 81dde697a5e6be7c5410f1344d4975b9536e19ea Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:19:51 +0800 Subject: [PATCH 42/44] Delete 'WorkingNote.java' --- WorkingNote.java | 532 ----------------------------------------------- 1 file changed, 532 deletions(-) delete mode 100644 WorkingNote.java diff --git a/WorkingNote.java b/WorkingNote.java deleted file mode 100644 index ebb8233..0000000 --- a/WorkingNote.java +++ /dev/null @@ -1,532 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.model; - -import android.appwidget.AppWidgetManager; -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; -import android.util.Log; - -import net.micode.notes.data.Notes; -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 { - // Note for the working note - // 当前工作笔记的 Note 对象 - private Note mNote; - // Note Id - // 笔记的唯一标识符 - private long mNoteId; - // Note content - // 笔记内容 - private String mContent; - // Note mode - // 笔记模式 - private int mMode; - // 提醒日期 - private long mAlertDate; - // 修改日期 - private long mModifiedDate; - // 背景颜色 ID - private int mBgColorId; - // 小部件 ID - private int mWidgetId; - // 小部件类型 - private int mWidgetType; - // 文件夹 ID - private long mFolderId; - // 应用程序上下文 - 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,// 数据的 MIME 类型 - DataColumns.DATA1,// 数据字段 1 - DataColumns.DATA2, // 数据字段 2 - DataColumns.DATA3, // 数据字段 3 - DataColumns.DATA4, // 数据字段 4 - }; -/* - * 笔记查询投影。 - * 该常量定义了在查询笔记时需要返回的列。 - * @see NoteColumns - */ - public static final String[] NOTE_PROJECTION = new String[] { - NoteColumns.PARENT_ID, // 笔记的父 ID(文件夹 ID) - NoteColumns.ALERTED_DATE, // 笔记的提醒日期 - NoteColumns.BG_COLOR_ID, // 笔记的背景颜色 ID - NoteColumns.WIDGET_ID, // 笔记的小部件 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; - - // New note construct - /* - * 创建一个新的空笔记。 - * 该构造函数用于初始化一个新的空笔记对象。 - * @param context 应用程序上下文。 - * @param folderId 文件夹 ID。 - */ - private WorkingNote(Context context, long folderId) { - // 设置应用程序上下文 - mContext = context; - mAlertDate = 0; // 初始化提醒日期为 0 - mModifiedDate = System.currentTimeMillis();// 设置修改日期为当前时间 - mFolderId = folderId;// 设置文件夹 ID - mNote = new Note();// 创建一个新的 Note 对象 - mNoteId = 0;// 初始化笔记 ID 为 0 - mIsDeleted = false;// 初始化删除状态为 false - mMode = 0;// 初始化笔记模式为 0 - mWidgetType = Notes.TYPE_WIDGET_INVALIDE;// 初始化小部件类型为无效类型 - } - - // Existing note construct - /* - * 加载现有的笔记 - * 该构造函数用于加载现有的笔记对象。 - * @param context 应用程序上下文。 - * @param noteId 笔记的唯一标识符。 - * @param folderId 文件夹 ID。 - */ - private WorkingNote(Context context, long noteId, long folderId) { - mContext = context;// 设置应用程序上下文 - mNoteId = noteId;// 设置笔记的唯一标识符 - mFolderId = folderId;// 设置文件夹 ID - mIsDeleted = false;// 初始化删除状态为 false - mNote = new Note(); // 创建一个新的 Note 对象 - loadNote();// 加载笔记数据 - } -/* - * 加载笔记数据。 - * 该方法用于从内容提供者中加载笔记数据,并将其设置到当前的 WorkingNote 对象中。 - */ - private void loadNote() { - // 查询笔记数据 - Cursor cursor = mContext.getContentResolver().query( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, - null, null); // 检查游标是否为空 - - if (cursor != null) { // 移动游标到第一行 - if (cursor.moveToFirst()) { - mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); // 获取文件夹 ID - mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); // 获取背景颜色 ID - mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);// 获取小部件 ID - mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); // 获取小部件类型 - mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);// 获取提醒日期 - mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);// 获取修改日期 - } - cursor.close(); // 关闭游标 - } else { // 记录错误日志 - Log.e(TAG, "No note with id:" + mNoteId); - // 抛出异常 - throw new IllegalArgumentException("Unable to find note with id " + mNoteId); - } - loadNoteData();// 加载笔记数据 - } -/* - * 加载笔记数据。 - * 该方法用于从内容提供者中加载笔记的具体数据,并将其设置到当前的 WorkingNote 对象中。 - */ - private void loadNoteData() {// 查询笔记数据 - Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, - DataColumns.NOTE_ID + "=?", new String[] { - String.valueOf(mNoteId) - }, null); // 检查游标是否为空 - - if (cursor != null) { // 移动游标到第一行 - if (cursor.moveToFirst()) { - do { // 获取数据的 MIME 类型 - String type = cursor.getString(DATA_MIME_TYPE_COLUMN); - if (DataConstants.NOTE.equals(type)) {// 根据 MIME 类型处理数据 - mContent = cursor.getString(DATA_CONTENT_COLUMN); // 如果是文本笔记,获取内容和模式 - mMode = cursor.getInt(DATA_MODE_COLUMN); - mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); - } else if (DataConstants.CALL_NOTE.equals(type)) { - mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); // 如果是通话笔记,获取通话数据 ID - } else { - Log.d(TAG, "Wrong note type with type:" + type); // 记录错误日志 - } - } while (cursor.moveToNext());// 移动到下一行 - } - cursor.close(); // 关闭游标 - } else { - Log.e(TAG, "No data with id:" + mNoteId); // 记录错误日志 - // 抛出异常 - throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); - } - } -/* - * 创建一个新的空笔记。 - * 该方法用于创建一个新的空笔记对象,并设置其背景颜色、小部件 ID 和小部件类型。 - * @param context 应用程序上下文。 - * @param folderId 文件夹 ID。 - * @param widgetId 小部件 ID。 - * @param widgetType 小部件类型。 - * @param defaultBgColorId 默认背景颜色 ID。 - * @return 新的 WorkingNote 对象。 - */ - public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, - int widgetType, int defaultBgColorId) { - WorkingNote note = new WorkingNote(context, folderId);// 创建一个新的空笔记对象 - note.setBgColorId(defaultBgColorId); // 设置背景颜色 ID - note.setWidgetId(widgetId);// 设置小部件 ID - note.setWidgetType(widgetType); // 设置小部件类型 - return note;// 返回新的 WorkingNote 对象 - } -/* - *加载现有的笔记。 - * 该方法用于加载现有的笔记对象。 - * @param context 应用程序上下文。 - * @param id 笔记的唯一标识符。 - * @return 加载的 WorkingNote 对象。 - */ - public static WorkingNote load(Context context, long id) { - return new WorkingNote(context, id, 0); // 返回一个新的 WorkingNote 对象,使用指定的上下文和笔记 ID - } -/* - * 保存笔记。 - * 该方法用于保存当前的笔记对象。如果笔记值得保存,则将其同步到数据库中,并更新小部件内容(如果存在小部件)。 - * @return 如果保存成功,则返回 true;否则返回 false。 - */ - public synchronized boolean saveNote() { - if (isWorthSaving()) { // 检查笔记是否值得保存 - if (!existInDatabase()) { // 如果笔记不存在于数据库中 - if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { - // 获取新的笔记 ID - Log.e(TAG, "Create new note fail with id:" + mNoteId);// 记录错误日志 - return false; - } - } - - mNote.syncNote(mContext, mNoteId);// 同步笔记到数据库 - - /** - * Update widget content if there exist any widget of this note - * 如果存在该笔记的小部件,更新小部件内容。 - */ - if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID - && mWidgetType != Notes.TYPE_WIDGET_INVALIDE - && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); - } - return true; - } else { - return false; - } - } -/* - * 检查笔记是否存在于数据库中。 - * 该方法用于检查当前笔记对象是否已经存在于数据库中。 - * @return 如果笔记存在于数据库中,则返回 true;否则返回 false。 - */ - public boolean existInDatabase() {// 如果笔记 ID 大于 0,表示笔记存在于数据库中 - return mNoteId > 0; - } -/* - * 检查笔记是否值得保存。 - * 该方法用于检查当前笔记对象是否值得保存。如果笔记已被删除、内容为空且不存在于数据库中,或者笔记存在于数据库中但没有本地修改,则不值得保存。 - * @return 如果笔记值得保存,则返回 true;否则返回 false。 - */ - private boolean isWorthSaving() { - // 如果笔记已被删除,或者内容为空且不存在于数据库中,或者笔记存在于数据库中但没有本地修改,则不值得保存 - if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) - || (existInDatabase() && !mNote.isLocalModified())) { - return false; - } else { - return true; - } - } -/* - * 设置笔记设置状态监听器。 - * 该方法用于设置笔记设置状态的监听器,以便在笔记设置状态发生变化时通知监听器。 - * @param l 笔记设置状态监听器。 - */ - public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { - mNoteSettingStatusListener = l; // 设置笔记设置状态监听器 - } -/* - * 设置提醒日期。 - * 该方法用于设置笔记的提醒日期。如果新的提醒日期与当前提醒日期不同,则更新提醒日期,并通知笔记设置状态监听器。 - * @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); // 通知笔记设置状态监听器提醒日期已更改 - } - } -/* - * 标记笔记为已删除或未删除。 - * 该方法用于标记笔记为已删除或未删除。如果笔记存在小部件,并且笔记设置状态监听器不为空,则通知笔记设置状态监听器小部件已更改。 - * @param mark 是否标记为已删除。 - */ - public void markDeleted(boolean mark) { - mIsDeleted = mark; // 标记笔记为已删除或未删除 - if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID // 如果笔记存在小部件,并且笔记设置状态监听器不为空 - && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { - // 通知笔记设置状态监听器小部件已更改 - mNoteSettingStatusListener.onWidgetChanged(); - } - } -/* - * 设置背景颜色 ID。 - * 该方法用于设置笔记的背景颜色 ID。如果新的背景颜色 ID 与当前背景颜色 ID 不同,则更新背景颜色 ID,并通知笔记设置状态监听器背景颜色已更改。 - * @param id 背景颜色 ID。 - */ - public void setBgColorId(int id) { - if (id != mBgColorId) { // 如果新的背景颜色 ID 与当前背景颜色 ID 不同 - mBgColorId = id;// 更新背景颜色 ID - if (mNoteSettingStatusListener != null) { // 如果笔记设置状态监听器不为空 - mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知笔记设置状态监听器背景颜色已更改 - } - mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 设置笔记的背景颜色 ID - } - } -/* - * 设置检查列表模式。 - * 该方法用于设置笔记的检查列表模式。如果新的模式与当前模式不同,则更新模式,并通知笔记设置状态监听器检查列表模式已更改。 - * @param mode 检查列表模式。 - */ - public void setCheckListMode(int mode) {// 如果新的模式与当前模式不同 - if (mMode != mode) { // 如果笔记设置状态监听器不为空 - if (mNoteSettingStatusListener != null) { // 通知笔记设置状态监听器检查列表模式已更改 - mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); - } - mMode = mode;// 更新模式 - mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 设置笔记的检查列表模式 - } - } -/* - * 设置小部件类型。 - * 该方法用于设置笔记的小部件类型。如果新的类型与当前类型不同,则更新小部件类型,并将其保存到笔记数据中。 - * @param type 小部件类型。 - */ - public void setWidgetType(int type) { // 如果新的类型与当前类型不同 - if (type != mWidgetType) { // 更新小部件类型 - mWidgetType = type; - mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 设置笔记的小部件类型 - } - } -/* - * 设置小部件 ID。 - * 该方法用于设置笔记的小部件 ID。如果新的 ID 与当前 ID 不同,则更新小部件 ID,并将其保存到笔记数据中。 - * @param id 小部件 ID。 - */ - public void setWidgetId(int id) { // 如果新的 ID 与当前 ID 不同 - if (id != mWidgetId) { // 更新小部件 ID - mWidgetId = id; - mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 设置笔记的小部件 ID - } - } -/* - * 设置工作文本。 - * 该方法用于设置笔记的工作文本。如果新的文本与当前文本不同,则更新工作文本,并将其保存到笔记数据中。 - * @param text 工作文本。 - */ - public void setWorkingText(String text) { // 如果新的文本与当前文本不同 - if (!TextUtils.equals(mContent, text)) { // 更新工作文本 - mContent = text; - mNote.setTextData(DataColumns.CONTENT, mContent);// 设置笔记的工作文本 - } - } -/* - * 将笔记转换为通话笔记。 - * 该方法用于将笔记转换为通话笔记。它设置通话日期、电话号码和父文件夹 ID。 - * @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)); // 设置父文件夹 ID - } -/* - * 检查是否有提醒。 - * 该方法用于检查笔记是否有提醒。如果提醒日期大于 0,则表示有提醒。 - * @return 如果有提醒,则返回 true;否则返回 false。 - */ - 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。 - * 该方法用于获取笔记的唯一标识符。 - * @return 笔记 ID。 - */ - public long getNoteId() { - return mNoteId; - } -/* - * 获取文件夹 ID。 - * 该方法用于获取笔记所属的文件夹 ID。 - * @return 文件夹 ID。 - */ - public long getFolderId() { - return mFolderId; - } -/* - * 获取小部件 ID。 - * 该方法用于获取笔记的小部件 ID。 - * @return 小部件 ID。 - */ - public int getWidgetId() { - return mWidgetId; - } -/* - * 获取小部件类型。 - * 该方法用于获取笔记的小部件类型。 - * @return 小部件类型。 - */ - public int getWidgetType() { - return mWidgetType; - } -/* - * 笔记设置状态监听器接口。 - * 该接口定义了笔记设置状态发生变化时的回调方法。 - */ - public interface NoteSettingChangedListener { - /** - * Called when the background color of current note has just changed - * 当当前笔记的背景颜色发生变化时调用。 - */ - void onBackgroundColorChanged(); - - /** - * Called when user set clock - * 当用户设置提醒时调用。 - * @param date 提醒日期。 - * @param set 是否设置提醒 - */ - void onClockAlertChanged(long date, boolean set); - - /** - * Call when user create note from widget - * 当用户从小部件创建笔记时调用。 - */ - void onWidgetChanged(); - - /** - * Call when switch between check list mode and normal mode - * @param oldMode is previous mode before change - * @param newMode is new mode - * 当用户在检查列表模式和普通模式之间切换时调用。 - * @param oldMode 之前的模式。 - * @param newMode 新的模式。 - */ - void onCheckListModeChanged(int oldMode, int newMode); - } -} -- 2.34.1 From f2e67f3581ff9b02fa5b69d8bdf7b48fff4218cd Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:20:01 +0800 Subject: [PATCH 43/44] Delete 'TaskList.java' --- TaskList.java | 465 -------------------------------------------------- 1 file changed, 465 deletions(-) delete mode 100644 TaskList.java diff --git a/TaskList.java b/TaskList.java deleted file mode 100644 index 4c6b5bd..0000000 --- a/TaskList.java +++ /dev/null @@ -1,465 +0,0 @@ -/* - * 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; - -/* - * TaskList类表示一个任务列表,继承自Node类。 - * 该类包含了任务列表的各种属性和操作方法,如创建、更新、设置内容等。 - */ -public class TaskList extends Node { - private static final String TAG = TaskList.class.getSimpleName(); - - private int mIndex; // 任务列表的索引 - - private ArrayList mChildren; // 子任务列表 -/* - * 构造函数,初始化任务列表对象。 - */ - public TaskList() { - super(); - mChildren = new ArrayList(); - mIndex = 1; - } -/* - * 生成创建任务列表的JSON对象。 - * @param actionId 操作ID - * @return 包含创建任务列表信息的JSONObject - */ - public JSONObject getCreateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - // 设置操作类型为创建 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - - // action_id - // 设置操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // index - // 设置索引 - js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); - - // entity_delta - // 设置实体信息 - 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); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate tasklist-create jsonobject"); - } - - return js; - } -/* - * 生成更新任务列表的JSON对象。 - * @param actionId 操作ID - * @return 包含更新任务列表信息的JSONObject - */ - public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - // 设置操作类型为更新 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - - // action_id - // 设置操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // id - // 设置任务列表的ID - js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - - // entity_delta - // 设置任务列表的实体信息 - 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); - - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - // 抛出自定义异常,表示生成JSON对象失败 - throw new ActionFailureException("fail to generate tasklist-update jsonobject"); - } - - return js; // 返回包含更新任务列表信息的JSONObject - } -/* - * 根据远程JSON对象设置任务列表的内容。 - * @param js 包含任务列表信息的远程JSON对象 - */ - public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { - try { - // id - // 如果JSON对象中包含ID字段,则设置任务列表的ID - if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { - setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); - } - - // last_modified - // 如果JSON对象中包含最后修改时间字段,则设置任务列表的最后修改时间 - if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { - setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); - } - - // name - // 如果JSON对象中包含名称字段,则设置任务列表的名称 - if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { - setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); - } - - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - // 抛出自定义异常,表示从JSON对象获取任务列表内容失败 - throw new ActionFailureException("fail to get tasklist content from jsonobject"); - } - } - } -/* - * 根据本地JSON对象设置任务列表的内容。 - * @param js 包含任务列表信息的本地JSON对象 - */ - public void setContentByLocalJSON(JSONObject js) { - // 检查JSON对象是否为空或是否包含必要的字段 - if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { - Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); - } - - 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) { - // 如果是系统文件夹类型,根据文件夹ID设置任务列表名称 - 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) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } -/* - * 根据任务列表内容生成本地JSON对象。 - * @return 包含任务列表内容的本地JSON对象,如果发生错误则返回null - */ - public JSONObject getLocalJSONFromContent() { - try { - // 创建一个新的JSON对象 - JSONObject js = new JSONObject(); - JSONObject folder = new JSONObject(); - // 获取任务列表的名称 - String folderName = getName(); - // 如果名称以GTaskStringUtils.MIUI_FOLDER_PREFFIX开头,则去掉前缀 - 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); - // 将文件夹信息添加到JSON对象中 - js.put(GTaskStringUtils.META_HEAD_NOTE, folder); - // 返回包含任务列表内容的本地JSON对象 - return js; - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - // 如果发生错误,返回null - return null; - } - } -/* - * 获取同步操作类型。 - * @param c 包含本地笔记信息的Cursor对象 - * @return 同步操作类型 - */ - public int getSyncAction(Cursor c) { - try { - // 如果本地没有修改 - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update - // 如果没有更新,返回SYNC_ACTION_NONE - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side - return SYNC_ACTION_NONE; - } else { - // 应用远程更新到本地 - // apply remote to local - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // 验证Google任务ID是否匹配 - // validate gtask id - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; - } - // 如果同步ID与最后修改时间匹配,表示只有本地修改 - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only - return SYNC_ACTION_UPDATE_REMOTE; - } else { - // for folder conflicts, just apply local modification - // 对于文件夹冲突,直接应用本地修改 - return SYNC_ACTION_UPDATE_REMOTE; - } - } - } catch (Exception e) { - Log.e(TAG, e.toString()); - // 捕获异常并记录错误日志 - e.printStackTrace(); - } - - return SYNC_ACTION_ERROR; // 默认返回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) { - // need to set prior sibling and parent - // 需要设置前一个兄弟任务和父任务 - 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); - - // update the task list - // 更新任务列表 - Task preTask = null; - Task afterTask = null; - if (index != 0) - preTask = mChildren.get(index - 1); - if (index != mChildren.size() - 1) - afterTask = mChildren.get(index + 1); - // 设置任务的前一个兄弟任务 - task.setPriorSibling(preTask); - // 如果存在后一个兄弟任务,则更新其后一个兄弟任务的前一个兄弟任务 - if (afterTask != null) - afterTask.setPriorSibling(task); - } - - return 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) { - // reset prior sibling and parent - // 重置任务的前一个兄弟任务和父任务 - task.setPriorSibling(null); - task.setParent(null); - - // update the task list - // 更新任务列表 - if (index != mChildren.size()) { - // 如果移除的任务不是最后一个任务,则更新其后一个任务的前一个兄弟任务 - mChildren.get(index).setPriorSibling( - index == 0 ? null : mChildren.get(index - 1)); - } - } - } - return ret; - } -/* - * 将子任务移动到指定索引位置。 - * @param task 要移动的子任务 - * @param index 目标索引位置 - * @return 如果成功移动子任务,返回true;否则返回false - */ - public boolean 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) { - // 遍历子任务列表 - for (int i = 0; i < mChildren.size(); i++) { - Task t = mChildren.get(i); - // 如果任务的GID与传入的GID匹配,则返回该任务 - if (t.getGid().equals(gid)) { - return t; - } - } - // 如果未找到匹配的任务,返回null - return 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 getChilTaskByGid(String gid) { - for (Task task : mChildren) { - if (task.getGid().equals(gid)) - return task; - } - return null; - } -/* - * 获取子任务列表。 - * @return 子任务列表 - */ - public ArrayList getChildTaskList() { - return this.mChildren; - } -/* - * 设置任务列表的索引。 - * @param index 任务列表的索引 - */ - public void setIndex(int index) { - this.mIndex = index; - } -/* - * 获取任务列表的索引。 - * @return 任务列表的索引 - */ - public int getIndex() { - return this.mIndex; - } -} -- 2.34.1 From a033586fcb5ed05762994a645f3833e3be03e31f Mon Sep 17 00:00:00 2001 From: pcoifxtab <1508386176@qq.com> Date: Wed, 25 Dec 2024 13:20:09 +0800 Subject: [PATCH 44/44] Delete 'Task.java' --- Task.java | 454 ------------------------------------------------------ 1 file changed, 454 deletions(-) delete mode 100644 Task.java diff --git a/Task.java b/Task.java deleted file mode 100644 index 8314242..0000000 --- a/Task.java +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.data; - -import android.database.Cursor; -import android.text.TextUtils; -import android.util.Log; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/* - * Task类表示一个任务,继承自Node类。 - * 该类包含了任务的各种属性和操作方法,如创建、更新、设置内容等。 - */ -public class Task extends Node { - private static final String TAG = Task.class.getSimpleName(); - - private boolean mCompleted; // 任务是否已完成 - - private String mNotes; // 任务的备注 - - private JSONObject mMetaInfo; // 任务的元数据信息 - - private Task mPriorSibling; // 前一个兄弟任务 - - private TaskList mParent; // 父任务列表 -/* - * 构造函数,初始化任务对象。 - * 初始化任务的完成状态、备注、前一个兄弟任务、父任务列表和元数据信息。 - */ - public Task() { - super(); // 调用父类Node的构造函数 - mCompleted = false; // 初始化任务为未完成状态 - mNotes = null; // 初始化备注为空 - mPriorSibling = null; // 初始化前一个兄弟任务为空 - mParent = null; // 初始化父任务列表为空 - mMetaInfo = null; // 初始化元数据信息为空 - } -/* - *生成创建任务的JSON对象。 - * @param actionId 操作ID - * @return 包含创建任务信息的JSONObject - */ - public JSONObject getCreateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - // 设置操作类型为创建 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - - // action_id - // 设置操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // index - // 设置任务在父任务列表中的索引 - js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); - - // entity_delta - // 设置任务的实体信息 - 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); - - // parent_id - // 设置父任务列表的ID - js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); - - // dest_parent_type - // 设置目标父类型 - js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_GROUP); - - // list_id - // 设置任务列表的ID - js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); - - // prior_sibling_id - // 设置前一个兄弟任务的ID - if (mPriorSibling != null) { - js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); - } - - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - // 抛出自定义异常,表示生成JSON对象失败 - throw new ActionFailureException("fail to generate task-create jsonobject"); - } - - return js; // 返回包含创建任务信息的JSONObject - } -/* - * 生成更新任务的JSON对象。 - * @param actionId 操作ID - * @return 包含更新任务信息的JSONObject - */ - public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - // 设置操作类型为更新 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - - // action_id - // 设置操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // id - // 设置任务的ID - js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - - // entity_delta - // 设置任务的实体信息 - 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); - - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - // 抛出自定义异常,表示生成JSON对象失败 - throw new ActionFailureException("fail to generate task-update jsonobject"); - } - - return js; // 返回包含更新任务信息的JSONObject - } -/* - * 根据远程JSON对象设置任务的内容。 - * @param js 包含任务信息的远程JSON对象 - */ - public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { - try { - // id - // 如果JSON对象中包含ID字段,则设置任务的ID - if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { - setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); - } - - // last_modified - // 如果JSON对象中包含最后修改时间字段,则设置任务的最后修改时间 - if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { - setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); - } - - // name - // 如果JSON对象中包含名称字段,则设置任务的名称 - if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { - setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); - } - - // notes - // 如果JSON对象中包含备注字段,则设置任务的备注 - if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { - setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); - } - - // deleted - // 如果JSON对象中包含删除状态字段,则设置任务的删除状态 - if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { - setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); - } - - // completed - // 如果JSON对象中包含完成状态字段,则设置任务的完成状态 - if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { - setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); - } - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - // 抛出自定义异常,表示从JSON对象获取任务内容失败 - throw new ActionFailureException("fail to get task content from jsonobject"); - } - } - } -/* - * 根据本地JSON对象设置任务的内容。 - * @param js 包含任务信息的本地JSON对象 - */ - public void setContentByLocalJSON(JSONObject js) { - // 检查JSON对象是否为空或是否包含必要的字段 - if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) - || !js.has(GTaskStringUtils.META_HEAD_DATA)) { - Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); - } - - try { - // 获取笔记的元数据 - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - // 获取数据数组 - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - // 检查笔记的类型是否为普通笔记类型 - if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { - Log.e(TAG, "invalid type"); - return; - } - // 遍历数据数组,查找MIME类型为DataConstants.NOTE的数据 - 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) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } -/* - * 根据任务内容生成本地JSON对象。 - * @return 包含任务内容的本地JSON对象,如果发生错误则返回null - */ - public JSONObject getLocalJSONFromContent() { - String name = getName(); - try { - if (mMetaInfo == null) { - // new task created from web - // 如果是从网页创建的新任务 - if (name == null) { - Log.w(TAG, "the note seems to be an empty one"); - return null; - } - // 创建一个新的JSON对象 - JSONObject js = new JSONObject(); - JSONObject note = new JSONObject(); - JSONArray dataArray = new JSONArray(); - JSONObject data = new JSONObject(); - data.put(DataColumns.CONTENT, name); - dataArray.put(data); - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - return js; - } else { - // synced task - // 如果是已同步的任务 - JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - // 遍历数据数组,更新任务名称 - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - data.put(DataColumns.CONTENT, getName()); - break; - } - } - - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - return mMetaInfo; - } - } catch (JSONException e) { - // 捕获JSON异常并记录错误日志 - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; - } - } -/* - * 设置任务的元数据信息。 - * @param metaData 包含元数据信息的MetaData对象 - */ - public void setMetaInfo(MetaData metaData) { - // 检查MetaData对象是否为空,并且是否包含笔记信息 - if (metaData != null && metaData.getNotes() != null) { - try { - // 将笔记信息转换为JSONObject对象 - mMetaInfo = new JSONObject(metaData.getNotes()); - } catch (JSONException e) { - // 捕获JSON异常并记录警告日志 - Log.w(TAG, e.toString()); - mMetaInfo = null; - // 如果转换失败,将mMetaInfo设置为null - } - } - } -/* - * 获取同步操作类型。 - * @param c 包含本地笔记信息的Cursor对象 - * @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字段,表示远程笔记ID已被删除 - if (!noteInfo.has(NoteColumns.ID)) { - Log.w(TAG, "remote note id seems to be deleted"); - return SYNC_ACTION_UPDATE_LOCAL; - } - - // validate the note id now - // 验证笔记ID是否匹配 - if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { - Log.w(TAG, "note id doesn't match"); - return SYNC_ACTION_UPDATE_LOCAL; - } - - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update - // 如果本地没有修改 - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side - // 如果没有更新,返回SYNC_ACTION_NONE - return SYNC_ACTION_NONE; - } else { - // apply remote to local - // 应用远程更新到本地 - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // validate gtask id - // 验证Google任务ID是否匹配 - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; - } - // 如果同步ID与最后修改时间匹配,表示只有本地修改 - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only - 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; // 默认返回SYNC_ACTION_ERROR - } -/* - * 判断任务是否值得保存。 - * @return 如果任务值得保存,返回true;否则返回false - */ - public boolean isWorthSaving() { - // 检查元数据信息是否存在,或者任务名称和备注是否不为空且不为空白字符串 - return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) - || (getNotes() != null && getNotes().trim().length() > 0); - } -/* - * 设置任务的完成状态。 - * @param completed 任务的完成状态,true表示已完成,false表示未完成 - */ - public void setCompleted(boolean completed) { - this.mCompleted = completed; - } -/* - * 设置任务的备注。 - * @param notes 任务的备注 - */ - public void setNotes(String notes) { - this.mNotes = notes; - } -/* - * 设置前一个兄弟任务。 - * @param priorSibling 前一个兄弟任务 - */ - public void setPriorSibling(Task priorSibling) { - this.mPriorSibling = priorSibling; - } -/* - * 设置父任务列表。 - * @param parent 父任务列表 - */ - public void setParent(TaskList parent) { - this.mParent = parent; - } -/* - * 获取任务的完成状态。 - * @return 任务的完成状态,true表示已完成,false表示未完成 - */ - public boolean getCompleted() { - return this.mCompleted; - } -/* - * 获取任务的备注。 - * @return 任务的备注 - */ - public String getNotes() { - return this.mNotes; - } -/* - * 获取前一个兄弟任务。 - * @return 前一个兄弟任务 - */ - public Task getPriorSibling() { - return this.mPriorSibling; - } -/* - * 获取父任务列表。 - * @return 父任务列表 - */ - public TaskList getParent() { - return this.mParent; - } - -} -- 2.34.1