From e3642bec52b48b38d1e08dc8b14f32aaf94f62e3 Mon Sep 17 00:00:00 2001 From: git1 <2125449565@qq.com> Date: Thu, 26 Dec 2024 22:43:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ActionFailureException.java | 87 ++++ Contact.java | 119 +++++ MetaData.java | 131 ++++++ NetworkFailureException.java | 88 ++++ Node.java | 137 ++++++ Notes.java | 355 ++++++++++++++ NotesDatabaseHelper.java | 462 +++++++++++++++++++ NotesProvider.java | 382 ++++++++++++++++ SqlData.java | 257 +++++++++++ SqlNote.java | 865 +++++++++++++++++++++++++++++++++++ Task.java | 696 ++++++++++++++++++++++++++++ TaskList.java | 747 ++++++++++++++++++++++++++++++ 12 files changed, 4326 insertions(+) create mode 100644 ActionFailureException.java create mode 100644 Contact.java create mode 100644 MetaData.java create mode 100644 NetworkFailureException.java create mode 100644 Node.java create mode 100644 Notes.java create mode 100644 NotesDatabaseHelper.java create mode 100644 NotesProvider.java create mode 100644 SqlData.java create mode 100644 SqlNote.java create mode 100644 Task.java create mode 100644 TaskList.java diff --git a/ActionFailureException.java b/ActionFailureException.java new file mode 100644 index 0000000..e5c2748 --- /dev/null +++ b/ActionFailureException.java @@ -0,0 +1,87 @@ +/* + * 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. + */ + +/* + 这段代码定义了一个名为 `ActionFailureException` 的自定义异常类,继承自 Java 的 `RuntimeException`。 + 它位于包 `net.micode.notes.gtask.exception` 中,并且属于开源项目的一部分。下面是对该代码的详细解释: + */ + + + // **包声明和导入** + package net.micode.notes.gtask.exception; + + //这行代码定义了该类所在的包名:`net.micode.notes.gtask.exception`。通常包用于组织类和接口,并为类提供命名空间。 + + //**类声明** + + public class ActionFailureException extends RuntimeException { + + //`ActionFailureException` 继承自 `RuntimeException`,意味着它是一个 **未检查异常(unchecked exception)**。 + // `RuntimeException` 是 `Exception` 类的子类,通常用于表示程序中的错误,通常不需要强制捕获或声明。 + //- 异常的名字 `ActionFailureException` 表示某种操作失败的情况。 + + //**序列化 ID** + private static final long serialVersionUID = 4425249765923293627L; + + /* + `serialVersionUID` 是一个用于序列化和反序列化过程中的版本控制的常量。 + - 在 Java 中,当一个类实现了 `Serializable` 接口时,`serialVersionUID` 用来确保反序列化时所读取的类版本与序列化时的类版本兼容。 + - 在这段代码中,`ActionFailureException` 类没有显式实现 `Serializable`,但这个字段可能是为以后可能的序列化需求预先定义的。 + + */ + + //**构造函数** + + // 默认构造函数 + + public ActionFailureException() { + super(); + } + + // 默认构造函数,调用 `RuntimeException` 的无参构造函数,表示没有具体的错误信息时抛出此异常。 + + //带消息构造函数 + + public ActionFailureException(String paramString) { + super(paramString); + } + + //- 这个构造函数允许传入一个字符串参数 `paramString`,用来描述异常的具体信息。这会调用 `RuntimeException` 的带有消息的构造函数。 + + //带消息和原因构造函数 + + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } + + //- 这个构造函数允许传入一个字符串参数和另一个异常(`Throwable`)作为参数。 + // 通过这种方式,可以指定异常的描述信息和导致当前异常的根本原因(例如,另一个异常)。 + + /* . **继承自 `RuntimeException` 的意义** + - `ActionFailureException` 继承自 `RuntimeException`,意味着它是一个 **非检查异常(unchecked exception)**。这类异常在编译时不强制要求捕获或声明,可以在程序运行时抛出。 + - 通常,非检查异常用于表示程序中存在逻辑错误或预料之外的情况(如空指针、数组越界等),并不要求调用者捕获。 + + **使用场景** + `ActionFailureException` 类可能用于表示某个操作(例如,数据库操作、网络请求、用户输入等)失败的情况。举例来说: + - 如果在应用中有一项任务失败,可能会抛出 `ActionFailureException`,并附带具体的错误信息或导致错误的根本异常。 + - 这个自定义异常提供了更多的灵活性,程序员可以通过不同的构造函数来传递错误信息或捕获更具体的错误原因。 + + + `ActionFailureException` 是一个自定义的运行时异常类,继承自 `RuntimeException`。 + - 它有三个构造函数,分别支持无参构造、传递错误信息、以及传递错误信息和原因异常。 + - 由于它是 `RuntimeException` 的子类,因此不强制要求捕获或声明,是用于处理程序中一些非预期错误的一个工具。 + + */ \ No newline at end of file diff --git a/Contact.java b/Contact.java new file mode 100644 index 0000000..138bebe --- /dev/null +++ b/Contact.java @@ -0,0 +1,119 @@ +/* + * 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;//导入Context类,用于访问应用环境 +import android.database.Cursor;// 导入 Cursor 类,用于操作查询结果 +import android.provider.ContactsContract.CommonDataKinds.Phone;// 导入电话常量 +import android.provider.ContactsContract.Data;// 导入数据常量 +import android.telephony.PhoneNumberUtils;// 导入电话工具类 +import android.util.Log; // 导入日志工具类 + +import java.util.HashMap;// 导入 HashMap 类,用于存储联系人缓存 + +public class Contact { // 定义 Contact 类 + private static HashMap sContactCache; + // 静态 HashMap 用于缓存联系人姓名 + private static final String TAG = "Contact"; + // 日志标签,用于标识日志信息 + 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 = '+')"; // 查询选项字符串,用于匹配来电显示的电话号码 + + //根据电话号码获取联系人姓名的方法 + public static String getContact(Context context, String phoneNumber) { + //初始化联系人缓存 + if(sContactCache == null) { + sContactCache = new HashMap(); + } + //如果缓存中已存在该电话号码,则直接返回对应的联系人姓名 + if(sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + //替换查询字符串中的“+”,使用电话号码的最小匹配形式 + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + //执行内容解析器查询,获取与电话号码匹配的联系人姓名 + Cursor cursor = context.getContentResolver().query( + //查询的 UIR + 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()); + // 返回 null + return null; + } finally { + // 确保游标被关闭以释放资源 + cursor.close(); + } + } else { // 如果没有找到匹配的联系人 + // 记录调试日志 + Log.d(TAG, "No contact matched with number:" + phoneNumber); + // 返回 null + return null; + } + } +} + + +/* +缓存检查: + +首先检查 sContactCache 是否已初始化,如果未初始化,则创建一个新的 HashMap。 +然后检查缓存中是否已经存有该电话号码的联系人姓名。如果有,直接返回缓存中的结果。 +查询联系人: + +如果缓存中没有该联系人,构建一个查询条件 selection,并用 PhoneNumberUtils.toCallerIDMinMatch(phoneNumber) 方法将 + 符号替换为号码的最小匹配形式。 +通过 context.getContentResolver().query 方法查询联系人数据库,查询条件是 selection,查询的列为联系人显示姓名(Phone.DISPLAY_NAME)。 +结果处理: + +如果查询返回有效的游标(即找到联系人信息),则尝试获取第一个结果的联系人姓名。 +将获取到的联系人姓名缓存起来,方便下次快速查询。 +错误处理: + +如果发生异常(例如游标获取数据时出现 IndexOutOfBoundsException),则记录错误日志并返回 null。 +如果查询没有返回任何联系人数据,则记录调试日志,并返回 null。 +资源释放: + +确保游标在操作完成后关闭,以释放数据库资源。 + 总结 +这段代码的功能是通过电话号码查询并返回对应的联系人姓名。为了提高效率,查询结果会被缓存,以避免重复查询数据库。代码充分利用了 ContentResolver 进行联系人数据的查询,并在出现问题时进行了异常处理和日志记录。 + +*/ \ No newline at end of file diff --git a/MetaData.java b/MetaData.java new file mode 100644 index 0000000..cef06db --- /dev/null +++ b/MetaData.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +//定义该类所在的包。包是 Java 中组织类的方式,有助于代码的组织和管理。 +package net.micode.notes.gtask.data; +//导入 Android 的 Cursor 类,Cursor 是用于访问和操作数据库查询结果的工具。 +import android.database.Cursor; +//导入 Android 的 Log 类,用于在代码中输出日志信息。 +import android.util.Log; + +//导入一个自定义的工具类 GTaskStringUtils,它包含与 Google Tasks 相关的常量和方法。 +import net.micode.notes.tool.GTaskStringUtils; + +//导入 JSON 处理类。JSONObject 用于创建和解析 JSON 数据,JSONException 用于捕获解析 JSON 时的错误。 +import org.json.JSONException; +import org.json.JSONObject; + + + +//定义一个公共类 MetaData,该类继承自 Task 类,表示与 Google 任务相关的元数据(例如任务的 gid)。 +public class MetaData extends Task { + + //定义一个 TAG 字符串常量,存储当前类的简短类名 "MetaData",用于在日志中输出调试信息。 + private final static String TAG = MetaData.class.getSimpleName(); + + + //声明一个私有成员变量 mRelatedGid,用于保存与任务相关的 Google 任务 ID(gid)。初始值为 null。 + private String mRelatedGid = null; + + +// 定义一个公共方法 setMeta,用于设置任务的元数据。接收两个参数: +// gid:一个字符串,表示任务的 Google ID。 +// metaInfo:一个 JSONObject,包含其他元数据(如任务的附加信息)。 + public void setMeta(String gid, JSONObject metaInfo) { + +// 尝试将传入的 gid 插入到 metaInfo 中。GTaskStringUtils.META_HEAD_GTASK_ID 是用于存储任务 ID 的常量。 +// 如果插入失败,捕获 JSONException 异常并打印错误日志。 + try { + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); + } + //将 metaInfo 转换为字符串并调用 setNotes() 方法保存。这将任务的元数据作为字符串保存在任务的 notes 字段中。 + setNotes(metaInfo.toString()); + //调用 setName() 设置任务的名称为 GTaskStringUtils.META_NOTE_NAME。这个名称可能是一个预定义的常量,表示任务的默认名称。 + setName(GTaskStringUtils.META_NOTE_NAME); + } + + + //定义一个公共方法 getRelatedGid,用于获取与任务相关的 Google 任务 ID。 + // return mRelatedGid;:返回之前保存的 mRelatedGid。 + public String getRelatedGid() { + return mRelatedGid; + } + + + + //@Override:表示该方法是重写父类 Task 中的方法。 + //public boolean isWorthSaving():定义一个方法 isWorthSaving,用于判断当前任务是否值得保存。通常,它会根据某些条件来判断任务是否需要被同步或保存到数据库中。 + //return getNotes() != null;:判断任务的 notes 字段是否为 null。如果 notes 字段不为空,说明任务有有效内容,返回 true;否则返回 false。 + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } + @Override + //定义方法 setContentByRemoteJSON,用于从远程获取 JSON 数据并设置任务内容。 + public void setContentByRemoteJSON(JSONObject js) { + //调用父类的同名方法来处理远程 JSON 数据的基本设置。 + super.setContentByRemoteJSON(js); + //检查任务的 notes 字段是否不为空。 + if (getNotes() != null) { + //如果 notes 不为空,尝试将其转换为 JSONObject。 + //然后,从中提取出 gid 并保存到 mRelatedGid 中。 + // 如果解析过程中出现错误(例如格式问题),会捕获 JSONException 并输出警告日志,同时将 mRelatedGid 设为 null。 + try { + JSONObject metaInfo = new JSONObject(getNotes().trim()); + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); + mRelatedGid = null; + } + } + } + + + //@Override: + //public void setContentByLocalJSON(JSONObject js): + //throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");: + @Override + //该方法定义了从本地 JSON 数据设置任务内容的行为。 + public void setContentByLocalJSON(JSONObject js) { + // this function should not be called + //该方法被设计为不可调用,如果被调用,抛出 IllegalAccessError 异常。 + // 该类不允许使用本地 JSON 数据设置任务内容,因此抛出异常来提醒开发者 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + + + //表示重写父类的方法。 + @Override + //该方法定义了获取本地 JSON 数据的行为。 + public JSONObject getLocalJSONFromContent() { + //与 setContentByLocalJSON 类似,该方法不应该被调用,如果被调用,抛出 IllegalAccessError 异常. + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + + + +//表示重写父类的方法。 + @Override + //定义获取同步操作的方法,通常会返回一个与同步操作相关的标志值。 + public int getSyncAction(Cursor c) { + //该方法不应被调用,抛出 IllegalAccessError 异常。 + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } + +} diff --git a/NetworkFailureException.java b/NetworkFailureException.java new file mode 100644 index 0000000..6e4486a --- /dev/null +++ b/NetworkFailureException.java @@ -0,0 +1,88 @@ +/* + * 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. + */ + + +/* + 这段代码定义了一个名为 NetworkFailureException 的自定义异常类,继承自 Java 的 Exception 类。 + 它位于包 net.micode.notes.gtask.exception 中,并且使用了 Apache 2.0 开源许可证。 + */ + +//包声明导入 +//这行代码声明了类 NetworkFailureException 所在的包: +// net.micode.notes.gtask.exception。包用于组织类和接口,避免命名冲突,并可以根据需要进行访问控制。 + +package net.micode.notes.gtask.exception; + + +/* + NetworkFailureException 继承自 Exception 类,这意味着它是一个 检查异常(checked exception)。 + 继承自 Exception 表示这是一个受检异常,调用者必须处理这种异常,通常通过 try-catch 块捕获或在方法签名中声明 throws。 + */ +public class NetworkFailureException extends Exception { + + /* + serialVersionUID 是用来确保类的序列化和反序列化过程中的版本兼容性。 + 当对象通过网络或 I/O 流进行序列化时,serialVersionUID 用于验证版本的匹配。 + 如果序列化的版本和类的版本不匹配,将抛出 InvalidClassException。 + 这个常量的值是自动生成的,也可以根据需要手动更改。 + */ + private static final long serialVersionUID = 2107610287180234136L; + + + /* + 构造默认函数 + 这个构造函数是默认构造函数,调用父类 Exception 的无参构造函数。表示没有提供错误信息时抛出该异常。 + */ + public NetworkFailureException() { + super(); + } + + + /* + 带消息的构造函数 + 这个构造函数允许传入一个字符串参数 paramString,用于描述异常的具体信息。这会调用父类 Exception 的带消息的构造函数。 + */ + public NetworkFailureException(String paramString) { + super(paramString); + } + + + /* + 带消息和原因的构造函数 + 这个构造函数允许传入一个字符串和一个 Throwable 对象,paramThrowable 表示导致当前异常的根本原因,通常用于链式异常。 + Throwable 是所有异常类的父类,表示抛出的异常,可以是 Exception 或 Error。 + */ + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} + + +/* + 受检异常(Checked Exception)的意义 + NetworkFailureException 继承自 Exception,它是一个 检查异常(checked exception)。与 RuntimeException 类的未检查异常不同,检查异常要求开发者在编译时处理它们。 + 对于网络相关的操作(例如网络请求、数据传输等),如果出现故障,通常会抛出 NetworkFailureException 来表明操作未成功。这类异常是检查性的,意味着调用者必须要么捕获它,要么在方法签名中声明 throws 以传递该异常。 + 使用场景 + NetworkFailureException 类可能用于表示网络相关的操作失败的情形。例如: + + 如果应用程序在进行网络请求时遇到故障(如超时、连接失败、无法访问服务器等),可以抛出 NetworkFailureException。 + 该异常可以携带具体的错误信息和/或网络失败的根本原因,帮助开发者定位问题。 + 总结 + NetworkFailureException 是一个自定义的检查异常类,继承自 Exception。 + 它有三个构造函数,分别支持无参构造、传递错误信息、以及传递错误信息和原因异常。 + 由于它是 Exception 的子类,它是一个受检异常,要求调用者显式地捕获或声明。 + 这个异常类可能用于表示网络相关的故障或错误,可以提供详细的错误信息和根本原因。 + */ \ No newline at end of file diff --git a/Node.java b/Node.java new file mode 100644 index 0000000..68eef35 --- /dev/null +++ b/Node.java @@ -0,0 +1,137 @@ +/* + * 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; + + +public abstract class Node { + //// 定义同步操作常量,表示不同的同步动作 + //这段代码定义了不同的常量,表示不同的同步操作类型。这些常量用于表示与远程和本地数据同步的动作。每个动作都用一个整数值标识, + // 例如:SYNC_ACTION_ADD_REMOTE 表示添加远程任务, + // SYNC_ACTION_UPDATE_LOCAL 表示更新本地任务, + // SYNC_ACTION_ERROR 表示同步错误等。 + 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; + + //mGid:一个字符串,存储任务的唯一标识符(Google ID),用于区分不同的任务。 + //mName:任务的名称,存储任务的标题或名称。 + //mLastModified:任务的最后修改时间,使用 long 类型(通常是 Unix 时间戳,表示自1970年1月1日以来的毫秒数)。 + //mDeleted:布尔值,表示该任务是否被删除。 + + // 任务的唯一标识符 + private String mGid; + // 任务的名称 + private String mName; + // 最后修改时间 + private long mLastModified; + // 任务是否已删除 + private boolean mDeleted; + + public Node() { + // 默认构造函数,初始化 mGid 为 null + mGid = null; + // 默认构造函数,初始化 mName 为空字符串 + mName = ""; + // 默认构造函数,初始化 mLastModified 为 0 + mLastModified = 0; + // 默认构造函数,初始化 mDeleted 为 false(表示任务未删除) + mDeleted = false; + } + + //getCreateAction(int actionId):根据给定的 actionId,返回一个 JSONObject,表示创建操作的同步动作。具体内容依赖子类实现。 + //getUpdateAction(int actionId):根据给定的 actionId,返回一个 JSONObject,表示更新操作的同步动作。具体内容依赖子类实现。 + //setContentByRemoteJSON(JSONObject js):从远程的 JSON 数据中设置内容。js 是传入的 JSONObject,该方法的具体实现依赖子类。 + //setContentByLocalJSON(JSONObject js):从本地的 JSON 数据中设置内容。js 是传入的 JSONObject,该方法的具体实现依赖子类。 + //getLocalJSONFromContent():将本地内容转换为 JSONObject,返回一个 JSON 对象。 + //getSyncAction(Cursor c):从 Cursor 对象中获取同步操作的动作,返回一个整数,表示当前同步操作的类型。 + public abstract JSONObject getCreateAction(int actionId); + + public abstract JSONObject getUpdateAction(int actionId); + + public abstract void setContentByRemoteJSON(JSONObject js); + + public abstract void setContentByLocalJSON(JSONObject js); + + public abstract JSONObject getLocalJSONFromContent(); + + public abstract int getSyncAction(Cursor c); + + + //这些是 Node 类的 setter 方法,用于设置成员变量的值: + + //setGid(String gid):设置任务的 Gid(Google ID)。 + //setName(String name):设置任务的名称。 + //setLastModified(long lastModified):设置任务的最后修改时间。 + //setDeleted(boolean deleted):设置任务是否被删除。 + public void setGid(String gid) { + this.mGid = gid; + } + + public void setName(String name) { + this.mName = name; + } + + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + + //这些是 Node 类的 getter 方法,用于获取成员变量的值: + + //getGid():返回任务的 Gid(Google ID)。 + //getName():返回任务的名称。 + //getLastModified():返回任务的最后修改时间。 + //getDeleted():返回任务是否已删除的布尔值。 + public String getGid() { + return this.mGid; + } + + public String getName() { + return this.mName; + } + + public long getLastModified() { + return this.mLastModified; + } + + public boolean getDeleted() { + return this.mDeleted; + } + +} diff --git a/Notes.java b/Notes.java new file mode 100644 index 0000000..023396f --- /dev/null +++ b/Notes.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.net.Uri; +// Notes 类用于定义与笔记管理相关的常量和数据结构 +public class Notes { + // 内容提供者的授权字符串,用于标识应用的数据 + public static final String AUTHORITY = "micode_notes"; + // 日志标记,用于标识与 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 以下id是系统文件夹的标识符 + * {@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 是用于存储通话记录 + */ + public static final int ID_ROOT_FOLDER = 0; + //根文件夹的id + public static final int ID_TEMPARAY_FOLDER = -1; + //临时笔记的id + public static final int ID_CALL_RECORD_FOLDER = -2; + //存储通话记录的id + public static final int ID_TRASH_FOLER = -3; + //回收站文件夹的id,用于存储删除的笔记 + + //提醒日期的键 + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + //背景颜色颜色id的键 + 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"; + //文件夹id的键 + 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; + //2x 大小的小部件类型 + public static final int TYPE_WIDGET_2X = 0; + //4x 大小的小部件类型 + public static final int TYPE_WIDGET_4X = 1; + + //内部类,用于保护与数据相关的常量,特别是笔记的类型 + 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 ,此 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 笔记表中行的唯一 id + *

Type: INTEGER (long)

类型为 INREGER (long) + */ + //唯一标识符的列名 + public static final String ID = "_id"; + + /** + * The parent's id for note or folder 笔记或文件夹的父 id类型· + *

Type: INTEGER (long)

+ */ + + //笔记或文件夹的父 id 名称 + public static final String PARENT_ID = "parent_id"; + + /** + * Created data for note or folder 笔记或文件夹的创建日期 + *

Type: INTEGER (long)

+ */ + + //创建日期的列名 + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date 笔记或文件夹的最新修改日期 + *

Type: INTEGER (long)

+ */ + // 修改日期的列名 + public static final String MODIFIED_DATE = "modified_date"; + + + /** + * Alert date 提醒日期 + *

Type: INTEGER (long)

+ */ + + //提醒日期的列名 + public static final String ALERTED_DATE = "alert_date"; + + /** + * Folder's name or text content of note 文件夹的名称或笔记的文本内容 + *

Type: TEXT

+ */ + //文本内容或文件夹名称的列名 + public static final String SNIPPET = "snippet"; + + /** + * Note's widget id 笔记的小部件 id + *

Type: INTEGER (long)

+ */ + //笔记小部件的列名 + public static final String WIDGET_ID = "widget_id"; + + /** + * Note's widget type 笔记的小部件类型 + *

Type: INTEGER (long)

+ */ + //笔记的小部件类型的列名 + public static final String WIDGET_TYPE = "widget_type"; + + /** + * Note's background color's id 笔记背景颜色 id + *

Type: INTEGER (long)

+ */ + //笔记的背景颜色 id 的列名 + 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

+ */ + // 表示笔记是否有附件。对于文本笔记,此值为0(无附件); + // 对于多媒体笔记,此值为1(至少有一个附件)。类型为整数。 + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * Folder's count of notes + *

Type: INTEGER (long)

+ */ + // 表示文件夹中笔记的数量,类型为整数(长整型), + // 用于跟踪文件夹内包含的笔记总数。 + public static final String NOTES_COUNT = "notes_count"; + + /** + * The file type: folder or note + *

Type: INTEGER

+ */ + // 表示文件的类型,可以是文件夹或笔记。类型为整数, + // 通过不同的整数值来区分文件夹和笔记。 + public static final String TYPE = "type"; + + /** + * The last sync id + *

Type: INTEGER (long)

+ */ + // 表示最后一次同步的ID。类型为整数(长整型), + // 用于标识最近的一次数据同步状态,以便进行版本控制。 + public static final String SYNC_ID = "sync_id"; + + /** + * Sign to indicate local modified or not + *

Type: INTEGER

+ */ + // 表示本地是否有修改的标志。类型为整数, + // 如果此值为1,表示本地数据已被修改;如果为0,则未修改。 + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * Original parent id before moving into temporary folder + *

Type : INTEGER

+ */ + // 表示在移动到临时文件夹之前的原始父级ID。类型为整数, + // 这有助于在需要恢复或参考原始位置时使用。 + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * The gtask id + *

Type : TEXT

+ */ + // 表示与 Google 任务(GTask)相关联的ID,类型为文本 + // 用于链接或引用与此笔记相关的Google任务 + public static final String GTASK_ID = "gtask_id"; + + /** + * The version code + *

Type : INTEGER (long)

+ */ + // 表示笔记或文件夹的版本号,类型为整数(长整型) + // 用于版本控制和管理更新,以确保数据的一致性 + public static final String VERSION = "version"; + } + + // DataColumns 接口用于统一管理与数据列相关的常量,这使得在多个类之间共享相同的列名和类型变得更加简洁和一致 + public interface DataColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + // 表示数据库中每一行的唯一标识符,类型为整数(长整型) + public static final String ID = "_id"; + + /** + * The MIME type of the item represented by this row. + *

Type: Text

+ */ + // 表示此行数据的MIME类型,用于描述数据的格式,类型为文本 + public static final String MIME_TYPE = "mime_type"; + + /** + * The reference id to note that this data belongs to + *

Type: INTEGER (long)

+ */ + // 表示与此数据相关联的笔记的ID,类型为整型 + public static final String NOTE_ID = "note_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + // 表示笔记或文件夹的创建日期,类型为整型 + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + // 表示笔记或文件夹的最后修改日期,类型为整型 + public static final String MODIFIED_DATE = "modified_date"; + + /** + * Data's content + *

Type: TEXT

+ */ + // 表示数据的具体内容,类型为文本型 + public static final String CONTENT = "content"; + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + // 通用数据列,具体含义取决于MIME类型,通常用于存储整数类型的数据,类型为整数 + public static final String DATA1 = "data1"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + // 通用数据列,具体含义取决于MIME类型,通常用于存储整数类型的数据,类型为整数 + public static final String DATA2 = "data2"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + // 通用数据列,具体含义取决于MIME类型,通常用于存储文本类型的数据,类型为文本 + public static final String DATA3 = "data3"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + // 通用数据列,具体含义取决于MIME类型,通常用于存储文本类型的数据,类型为文本 + public static final String DATA4 = "data4"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + // 通用数据列,具体含义取决于MIME类型,通常用于存储文本类型的数据,类型为文本 + public static final String DATA5 = "data5"; + } + + // 定义一个静态类 TextNote 实现 DataColumns 接口 + 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

+ */ + // 这一常量表示文本的模式,是检查列表模式还是普通模式,类型为整数 + 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"; + // 表示文本笔记内容的uri + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + } + + + // 定义一个静态类 CallNote 实现 DataColumns 接口 + public static final class CallNote implements DataColumns { + /** + * Call date for this record + *

Type: INTEGER (long)

+ */ + // 表示记录的通话日期,类型为长整型 + public static final String CALL_DATE = DATA1; + + /** + * Phone number for this record + *

Type: 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"; + + // 表示通话笔记的内容URI + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + } +} diff --git a/NotesDatabaseHelper.java b/NotesDatabaseHelper.java new file mode 100644 index 0000000..a5ef233 --- /dev/null +++ b/NotesDatabaseHelper.java @@ -0,0 +1,462 @@ +/* + * 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; +// SQLite数据库类 +import android.database.sqlite.SQLiteDatabase; +// SQLite数据库助手类 +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 +public class NotesDatabaseHelper extends SQLiteOpenHelper { + // 定义数据库名称,只能在 NotesDatabaseHelper 类内部访问,并且在程序运行过程中无法修改。 + private static final String DB_NAME = "note.db"; + // 数据库版本 + private static final int DB_VERSION = 4; + // 定义表名 TABLE 接口 + 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; + + // 在数据库创建笔记表 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + // 笔记 id,主键 + 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)," + + // 是否有附件,默认为 0 + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + // 修改日期,默认为当前时间,单位是毫秒 + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + // 笔记的数量,默认为 0 + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 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 ''," + // GTASK ID,默认为空字符串 + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本,默认为 0 + ")"; + + // 在数据库创建数据表 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + // 数据 id,主键 + 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,默认为空字符串 + ")"; + + // 创建索引以加速根据 NOTE_ID 查询的数据表 + 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 + */ + // 增加文件夹中的笔记计数,当笔记移动到该文件夹时 + 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 + ";" + // 按照新父级 id 更新 + " END"; // 结束触发器 + + /** + * Decrease folder's note count when move note from folder + */ + // 创建一个触发器,该触发器在更新笔记的父 id 字段后执行 + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + // 在更新父 id 字段时触发 + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + // 更新目标笔记的子笔记数量,减少 1 + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + // 条件:只在旧的父 id 等于当前笔记 id 时进行更新,且子笔记数量大于 0 + " 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 + */ + // 创建一个触发器,该触发器在向笔记表插入新笔记后执行 + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + // 在插入操作后触发 + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + // 更新目标笔记的子笔记数量,增加 1 + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + // 条件: 通过新插入笔记的父 id 找到对应的笔记并更新 + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * Decrease folder's note count when delete note from the folder + */ + // 创建一个触发器,该触发器在从笔记删除笔记后执行 + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + // 在删除操作后触发 + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + // 更新目标笔记的子笔记数量,数量减 1 + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + // 条件:当前笔记 id 等于 旧的父 id,并且子笔记数量大于 0 + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + /** + * Update note's content when insert data with type {@link DataConstants#NOTE} + */ + // 创建一个触发器,该触发器在向数据表插入新数据时执行 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + // 在插入操作后触发 + " AFTER INSERT ON " + TABLE.DATA + + // 当新插入的数据类型为 NOTE 时触发 + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + // 更新目标笔记的内容快照为新插入数据的内容 + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + // 条件:通过新插入数据的 NOTE_ID 找到对应的笔记进行更新 + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Update note's content when data with {@link DataConstants#NOTE} type has changed + */ + // 创建一个触发器,该触发器在更新数据表中的数据时执行 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + // 在更新操作后触发 + " AFTER UPDATE ON " + TABLE.DATA + + // 当旧数据类型为 NOTE 时触发 + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + // 更新目标笔记的内容快照为新更新数据的内容 + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + // 条件:通过新更新数据的 NOTE_ID 找到对应的笔记进行更新 + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Update note's content when data with {@link DataConstants#NOTE} type has deleted + */ + // 创建一个触发器,该触发器在删除数据表中的数据时执行。 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + // 在删除操作后触发 + " AFTER delete ON " + TABLE.DATA + + // 当旧数据类型为NOTE时触发 + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + // 将目标笔记的内容快照更新为空字符串 + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + // 条件:通过旧数据的NOTE_ID找到对应的笔记进行更新 + " 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 FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + // 在删除操作后触发 + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + // 在删除操作后触发 + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * Move notes belong to folder which has been moved to trash folder + */ + // 创建一个触发器,用于移动到垃圾箱文件夹的笔记 + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + // 在更新操作后触发 + // 当新父ID为垃圾箱文件夹时触发 + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + // 更新所有子笔记的父ID为垃圾箱文件夹ID + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // NotesDatabaseHelper类的构造函数,初始化数据库 + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + // 创建笔记表 + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); // 执行创建笔记表的SQL语句 + reCreateNoteTableTriggers(db); // 重新创建与笔记表相关的触发器 + createSystemFolder(db); // 创建系统文件夹 + Log.d(TAG, "note table has been created"); // 打印日志,指示笔记表已创建 + } + + // 重新创建笔记表的触发器 + 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); + } + + // 创建系统文件夹 + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + /** + * call record foler for call notes + */ + //创建通话记录文件夹用于存储通话笔记 + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); // 设置文件夹ID + 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); // 设置根文件夹ID + 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); // 设置临时文件夹ID + 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); //设置垃圾箱文件夹ID + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); //设置文件夹类型为系统 + db.insert(TABLE.NOTE, null, values); // 插入垃圾箱文件夹记录 + } + + //创建数据表的方法 + 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"); // 打印日志,指示数据表已创建 + } + + // 重新创建数据表的触发器的方法 + 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实例的静态同步方法 + static synchronized NotesDatabaseHelper getInstance(Context context) { + // 如果实例尚未创建,则创建一个新的实例 + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; // 返回现有或新创建的实例 + } + + // 数据库创建时调用的方法 + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); //创建笔记表 + createDataTable(db); // 创建数据表 + } + + // 数据库升级时调用的方法 + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; // 标识是否需要重新创建触发器 + boolean skipV2 = false; // 标识是否跳过v2的升级 + + // 从版本1升级到版本2 + if (oldVersion == 1) { + upgradeToV2(db); // 执行升级到v2的方法 + // 设置跳过v2的标志,因为此升级包含了从v2到v3的升级 + skipV2 = true; // this upgrade including the upgrade from v2 to v3 + oldVersion++; // 版本号自增 + } + + // 从版本2升级到版本3,如果没有跳过v2 + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); // 执行升级到v3的方法 + reCreateTriggers = true; // 设置需要重新创建触发器的标志 + oldVersion++; // 版本号自增 + } + + // 从版本3升级到版本4 + if (oldVersion == 3) { + upgradeToV4(db); // 执行升级到v4的方法 + oldVersion++; // 版本号自增 + } + + // 如果需要重新创建触发器,则执行相关方法 + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); // 重新创建笔记表的触发器 + reCreateDataTableTriggers(db); // 重新创建数据表的触发器 + } + + // 检查最终的版本号是否与目标版本一致 + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); // 抛出异常,表示升级失败 + } + } + + // 升级到版本2的方法 + 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的方法 + 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 在笔记表中添加用于谷歌任务的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(); // 创建ContentValues对象 + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); // 设置垃圾箱文件夹的ID + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); // 设置类型为系统文件夹 + db.insert(TABLE.NOTE, null, values); // 将垃圾箱文件夹插入到笔记表中 + } + + // 升级到版本4的方法 + private void upgradeToV4(SQLiteDatabase db) { + // 在笔记表中添加版本列 + //这行代码执行一个 SQL 命令,使用 ALTER TABLE 语句来修改 TABLE.NOTE 表 + //ADD COLUMN 指令用于添加新列 + //NoteColumns.VERSION 是新列的名称,应该是一个整型(INTEGER) + //NOT NULL 约束确保该列不能为空 + //DEFAULT 0 表示新列的默认值为0,这意味着如果未提供值,则该列会自动设置为0 + 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..de101bd --- /dev/null +++ b/NotesProvider.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.data; + + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; + +public class NotesProvider extends ContentProvider { + // UriMatcher用于根据URI识别请求的类型 + private static final UriMatcher mMatcher; + + // 数据库助手类,用于操作SQLite数据库 + private NotesDatabaseHelper mHelper; + + // 日志标签 + private static final String TAG = "NotesProvider"; + + // 定义URI匹配的常量 + private static final int URI_NOTE = 1; // 匹配"note" URI + private static final int URI_NOTE_ITEM = 2; // 匹配"note/#" URI,#表示ID + private static final int URI_DATA = 3; // 匹配"data" URI + private static final int URI_DATA_ITEM = 4; // 匹配"data/#" URI,#表示ID + private static final int URI_SEARCH = 5; // 匹配"search" URI,用于搜索功能 + private static final int URI_SEARCH_SUGGEST = 6; // 匹配搜索建议的URI + + // 静态代码块初始化UriMatcher + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); // 初始化UriMatcher,默认为NO_MATCH + // 添加不同的URI模式并与对应的常量匹配 + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // 匹配 "note" URI + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // 匹配 "note/#" URI,#是ID占位符 + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // 匹配 "data" URI + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // 匹配 "data/#" URI,#是ID占位符 + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // 匹配 "search" URI + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 匹配搜索建议URI + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); // 匹配搜索建议带查询字符串的URI + } + + /** + * 为了在搜索结果中展示更多信息,去除标题和内容中的换行符('\n')以及多余的空格。 + * x'0A'表示SQLite中的换行符('\n'),我们会去掉这些字符,确保搜索结果展示更加紧凑。 + */ + // 定义一个搜索投影,用于处理搜索结果中的显示字段 + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," // 查询笔记的ID + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," // 用ID作为搜索建议的附加数据 + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," // 去除换行符并去掉多余空格,作为搜索文本1 + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," // 同样处理第二个文本字段 + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," // 设置搜索结果的图标 + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," // 搜索建议的操作,设置为查看 + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; // 搜索建议的内容类型 + + // 定义搜索查询SQL语句 + private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION + + " FROM " + TABLE.NOTE // 查询Notes表中的数据 + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" // 根据笔记内容的摘要(SNIPPET)进行模糊查询 + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER // 排除属于垃圾箱的笔记 + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; // 仅返回普通笔记,而不是其他类型(如语音、图片等) + + @Override + public boolean onCreate() { + // 初始化数据库助手类 + mHelper = NotesDatabaseHelper.getInstance(getContext()); + return true; // 成功创建ContentProvider + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + // 定义一个Cursor对象用于返回查询结果 + Cursor c = null; + + // 获取可读的SQLiteDatabase实例 + SQLiteDatabase db = mHelper.getReadableDatabase(); + + // 用于存储从URI中获取的ID + String id = null; + + // 根据URI路径匹配不同的查询请求 + switch (mMatcher.match(uri)) { + // 匹配URI_NOTE,表示查询所有笔记 + case URI_NOTE: + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); + break; + + // 匹配URI_NOTE_ITEM,表示查询单个笔记(根据ID查询) + case URI_NOTE_ITEM: + // 从URI路径中获取笔记的ID + id = uri.getPathSegments().get(1); + + // 执行查询,条件是笔记ID与URI中提供的ID匹配,并且可以追加其他选择条件 + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + parseSelection(selection), + selectionArgs, null, null, sortOrder); + break; + + // 匹配URI_DATA,表示查询所有数据 + case URI_DATA: + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, sortOrder); + break; + + // 匹配URI_DATA_ITEM,表示查询单条数据(根据ID查询) + case URI_DATA_ITEM: + // 从URI路径中获取数据的ID + id = uri.getPathSegments().get(1); + + // 执行查询,条件是数据ID与URI中提供的ID匹配,并且可以追加其他选择条件 + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection), + selectionArgs, null, null, sortOrder); + break; + + // 匹配URI_SEARCH和URI_SEARCH_SUGGEST,表示处理搜索请求 + 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; + + // 如果是搜索建议的URI(URI_SEARCH_SUGGEST),则从URI路径中提取查询字符串 + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); + } + } else { + // 否则,从查询参数中提取搜索模式("pattern") + searchString = uri.getQueryParameter("pattern"); + } + + // 如果没有提供搜索字符串,则返回null + if (TextUtils.isEmpty(searchString)) { + return null; + } + + // 对搜索字符串进行格式化,添加通配符,确保模糊查询 + try { + searchString = String.format("%%%s%%", searchString); + + // 执行原生SQL查询,查询笔记摘要中符合条件的记录 + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); + } catch (IllegalStateException ex) { + // 捕获异常并打印日志 + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + + // 如果没有匹配的URI路径,抛出非法参数异常 + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // 如果查询成功并返回了Cursor对象,设置通知URI + if (c != null) { + // 通知ContentResolver该URI的内容发生了变化,ContentResolver会根据这个信息更新UI + c.setNotificationUri(getContext().getContentResolver(), uri); + } + + // 返回查询结果 + return c; + } + + + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写的数据库实例 + long dataId = 0, noteId = 0, insertedId = 0; // 初始化id变量,插入的ID将存储在这些变量中 + + // 根据URI路径匹配来判断要插入的数据类型 + switch (mMatcher.match(uri)) { + // 如果是URI_NOTE,插入一条新的笔记 + case URI_NOTE: + insertedId = noteId = db.insert(TABLE.NOTE, null, values); // 向笔记表插入数据,返回插入的ID + break; + + // 如果是URI_DATA,插入一条新的数据项 + case URI_DATA: + // 检查values中是否包含Note ID,这是数据与笔记的关联键 + if (values.containsKey(DataColumns.NOTE_ID)) { + noteId = values.getAsLong(DataColumns.NOTE_ID); // 获取关联的笔记ID + } else { + // 如果没有提供笔记ID,则打印日志 + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + // 向数据表插入数据,返回插入的ID + insertedId = dataId = db.insert(TABLE.DATA, null, values); + break; + + // 如果URI未知,抛出非法参数异常 + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // 如果插入的笔记ID大于0,通知ContentResolver该笔记数据已发生变化 + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // 如果插入的数据ID大于0,通知ContentResolver该数据项已发生变化 + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + // 返回插入的记录的URI,通常是ContentProvider的URI附加上插入的ID + return ContentUris.withAppendedId(uri, insertedId); + } + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; // 记录删除的行数 + String id = null; // 存储从URI中提取的ID + SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写的数据库实例 + boolean deleteData = false; // 标记是否删除了数据项 + + // 根据URI路径匹配来判断删除的对象类型 + switch (mMatcher.match(uri)) { + // 删除所有笔记 + case URI_NOTE: + // 在原有的选择条件上附加“ID > 0”的限制,确保不会删除系统保留的笔记 + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); // 删除笔记表中的数据 + break; + + // 删除单个笔记(根据ID) + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); // 从URI路径中获取笔记ID + long noteId = Long.valueOf(id); // 转换为long类型的ID + if (noteId <= 0) { + break; // 如果ID小于等于0,跳过删除操作 + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); // 删除指定ID的笔记 + break; + + // 删除所有数据项 + case URI_DATA: + count = db.delete(TABLE.DATA, selection, selectionArgs); // 删除数据表中的数据 + deleteData = true; // 标记删除的是数据项 + break; + + // 删除单个数据项(根据ID) + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); // 从URI路径中获取数据ID + count = db.delete(TABLE.DATA, + DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); // 删除指定ID的数据项 + deleteData = true; // 标记删除的是数据项 + break; + + // 如果URI无法匹配,抛出非法参数异常 + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // 如果删除成功,且删除的是数据项,则通知更新笔记相关数据 + if (count > 0) { + if (deleteData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); // 通知ContentResolver相关数据已变化 + } + + return count; // 返回删除的行数 + } + + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; // 记录更新的行数 + String id = null; // 用于存储从URI中提取的ID + SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写的数据库实例 + boolean updateData = false; // 标记是否更新了数据项 + + // 根据URI匹配来决定更新操作的具体内容 + switch (mMatcher.match(uri)) { + case URI_NOTE: + // 对笔记表进行更新时,调用increaseNoteVersion方法,更新笔记版本号 + increaseNoteVersion(-1, selection, selectionArgs); + count = db.update(TABLE.NOTE, values, selection, selectionArgs); // 执行更新操作 + break; + + case URI_NOTE_ITEM: + // 更新指定ID的笔记项 + 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的数据项 + id = uri.getPathSegments().get(1); // 从URI路径中获取数据ID + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); // 执行更新操作 + updateData = true; // 标记更新了数据项 + break; + + default: + // 如果URI无法匹配,抛出异常 + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // 如果更新成功(即有行被更新),则通知ContentResolver相关数据已变化 + if (count > 0) { + // 如果更新的是数据项,通知相关笔记数据变化 + if (updateData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + // 通知ContentResolverURI发生变化,触发UI更新 + getContext().getContentResolver().notifyChange(uri, null); + } + + return count; // 返回更新的行数 + } + + //该方法用于解析传入的selection条件,并确保如果selection不为空,则将其包裹在" AND (...)"内。这主要是为了构建更新时的SQL条件。 + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } + + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); // 增加版本号 + + // 如果指定了ID或selection,则追加WHERE条件 + 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); // 将最终的selection添加到SQL中 + } + + // 执行更新SQL语句 + mHelper.getWritableDatabase().execSQL(sql.toString()); + } diff --git a/SqlData.java b/SqlData.java new file mode 100644 index 0000000..0323a75 --- /dev/null +++ b/SqlData.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//package net.micode.notes.gtask.data;:定义当前类所属的包。 +//导入了与内容提供者交互的 ContentResolver、ContentUris、ContentValues、Cursor 等 Android 类, +// 还包括 JSON 处理类 JSONObject,以及自定义异常类 ActionFailureException。 +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; + + + + + +//字段解释: +//TAG:日志标识,通常用于调试时输出日志。 +//INVALID_ID:一个无效的 ID 值,通常用于初始化时表示数据未找到或未设置。 +//PROJECTION_DATA:定义了查询时返回的列,数据表的列与常量 DataColumns 中定义的列名相对应。 +//mContentResolver:ContentResolver 是用来访问 Android 内容提供者的工具。 +//mIsCreate:布尔值,表示当前 SqlData 对象是新创建的(true)还是从数据库中加载的(false)。 +//mDataId、mDataMimeType、mDataContent、mDataContentData1 和 mDataContentData3 用来存储与特定数据项相关的字段。 +//mDiffDataValues:用于存储不同数据项的差异,通常在插入或更新数据库时使用。 +public class SqlData { + private static final String TAG = SqlData.class.getSimpleName(); // 日志标识,用于调试和打印日志 + + private static final int INVALID_ID = -99999; // 无效的ID常量,用于初始化时标记无效ID + + // 定义用于查询数据表的投影列(PROJECTION),即查询结果中返回的列 + public static final String[] PROJECTION_DATA = new String[]{ + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + + // 定义数据表中各列的索引值(通过索引访问Cursor中的数据) + public static final int DATA_ID_COLUMN = 0; + public static final int DATA_MIME_TYPE_COLUMN = 1; + public static final int DATA_CONTENT_COLUMN = 2; + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + + private ContentResolver mContentResolver; // ContentResolver,用于访问和操作内容提供者中的数据 + + private boolean mIsCreate; // 标记当前对象是否是新创建的 + + // 存储数据的ID + private long mDataId; + // 存储数据的MIME类型 + private String mDataMimeType; + + // 存储数据的内容 + private String mDataContent; + // 存储数据的附加信息 + private long mDataContentData1; + // 存储数据的附加信息 + private String mDataContentData3; + + // 存储不同的数据值,用于更新或插入时的差异 + private ContentValues mDiffDataValues; + + + //构造函数: + //第一个构造函数用于创建一个新的 SqlData 实例,初始化一些默认值。 + //第二个构造函数接收一个 Cursor 对象,表示从数据库查询到的数据行。它将通过 loadFromCursor 方法加载 Cursor 中的数据。 + // 默认构造函数,用于创建一个新的 SqlData 实例 + public SqlData(Context context) { + // 获取 Context 中的 ContentResolver + mContentResolver = context.getContentResolver(); + // 标记这是一个新创建的实例 + mIsCreate = true; + // 初始化 ID 为无效 ID + mDataId = INVALID_ID; + // 设置默认的 MIME 类型 + mDataMimeType = DataConstants.NOTE; + // 初始化内容为空字符串 + mDataContent = ""; + // 初始化附加数据 1 为 0 + mDataContentData1 = 0; + // 初始化附加数据 3 为空字符串 + mDataContentData3 = ""; + // 初始化差异数据的 ContentValues 对象 + mDiffDataValues = new ContentValues(); + } + + // 构造函数,用于从 Cursor 中加载数据 + public SqlData(Context context, Cursor c) { + // 获取 Context 中的 ContentResolver + mContentResolver = context.getContentResolver(); + // 标记这个实例是从 Cursor 中加载数据而来的 + mIsCreate = false; + // 从 Cursor 中加载数据到对象的成员变量 + loadFromCursor(c); + // 初始化差异数据的 ContentValues 对象 + mDiffDataValues = new ContentValues(); + } + + + // 从 Cursor 中加载数据,填充到当前 SqlData 对象的成员变量中 + //loadFromCursor 方法: + //这个方法从数据库查询结果 Cursor 中获取数据,并填充到当前 SqlData 对象的成员变量中。 + //Cursor 是 Android 数据库查询结果的一个游标,getLong() 和 getString() 方法用于从 Cursor 中提取具体的列值,按照列的索引顺序获取。 + private void loadFromCursor(Cursor c) { + // 获取数据 ID + mDataId = c.getLong(DATA_ID_COLUMN); + // 获取数据的 MIME 类型 + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); + // 获取数据内容 + mDataContent = c.getString(DATA_CONTENT_COLUMN); + // 获取数据的附加信息 1 + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); + // 获取数据的附加信息 3 + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); // 获取数据的附加信息 3 + } + + + // 设置内容的方法,将传入的JSON对象中的数据提取并更新类的成员变量 + public void setContent(JSONObject js) throws JSONException { + // 获取ID,如果JSON中不存在则使用INVALID_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; + + // 获取MIME类型,如果JSON中没有,则使用默认值"NOTE" + 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; + + // 获取内容数据,如果JSON中没有,则使用空字符串 + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + // 如果是新创建或内容不同,则更新差异数据 + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; + + // 获取DATA1字段的数值,如果JSON中没有,则默认为0 + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + // 如果是新创建或DATA1不同,则更新差异数据 + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; + + // 获取DATA3字段的内容,如果JSON中没有,则使用空字符串 + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + // 如果是新创建或DATA3不同,则更新差异数据 + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; + } + + // 获取内容的方法,返回一个JSONObject,包含当前对象的各个字段 + 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并填充数据 + JSONObject js = new JSONObject(); + js.put(DataColumns.ID, mDataId); + js.put(DataColumns.MIME_TYPE, mDataMimeType); + js.put(DataColumns.CONTENT, mDataContent); + js.put(DataColumns.DATA1, mDataContentData1); + js.put(DataColumns.DATA3, mDataContentData3); + return js; + } + + // 提交数据到数据库,如果是新创建则插入,否则更新现有记录 + public void commit(long noteId, boolean validateVersion, long version) { + // 如果是新创建,插入数据 + if (mIsCreate) { + // 如果ID无效且有差异数据,移除ID + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + + // 添加noteId并执行插入操作 + 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 + public long getId() { + return mDataId; + } +} diff --git a/SqlNote.java b/SqlNote.java new file mode 100644 index 0000000..38174d1 --- /dev/null +++ b/SqlNote.java @@ -0,0 +1,865 @@ +/* + * 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. + */ + +// 包含了处理与Google任务(GTask)相关的数据操作的类,通常涉及数据的增、删、改、查等操作。 +package net.micode.notes.gtask.data; + +// 引入Android应用开发中所需的类库和工具 + +// 用于管理应用小部件 +import android.appwidget.AppWidgetManager; +// 用于通过内容提供者与数据库交互 +import android.content.ContentResolver; +// 存储数据行的值 +import android.content.ContentValues; +// 用于获取应用的上下文 +import android.content.Context; +// 用于操作数据库查询结果 +import android.database.Cursor; +// 用于操作数据的URI +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; + +// 引入工具类 +// 用于处理GTask字符串的工具类 +import net.micode.notes.tool.GTaskStringUtils; +// 用于解析资源文件的工具类 +import net.micode.notes.tool.ResourceParser; + +// 引入JSON处理相关的类 +// 处理JSON数组 +import org.json.JSONArray; +// 异常处理 +import org.json.JSONException; +// 处理JSON对象 +import org.json.JSONObject; + +// 引入一个常用的集合类 +// 用于操作动态数组 +import java.util.ArrayList; + + +public class SqlNote { + // 定义一个静态常量 TAG,用于记录日志时标识当前类,通常是类名 + private static final String TAG = SqlNote.class.getSimpleName(); + + // 定义一个静态常量 INVALID_ID,通常表示一个无效的ID值 + private static final int INVALID_ID = -99999; + + // 定义一个静态常量 PROJECTION_NOTE,这是一个字符串数组,包含了多个列名 + // 这些列名通常用于查询数据库时,作为SELECT语句中所需的数据列 + // 在这个例子中,列名可能与一个笔记(Note)相关的数据库表字段对应 + public static final String[] PROJECTION_NOTE = new String[] { + // 每个字符串代表数据库表中的一列,以下列举了相关列名 + // 笔记的ID + NoteColumns.ID, + // 提醒时间 + NoteColumns.ALERTED_DATE, + // 背景颜色ID + NoteColumns.BG_COLOR_ID, + // 创建时间 + NoteColumns.CREATED_DATE, + // 是否有附件 + NoteColumns.HAS_ATTACHMENT, + // 修改时间 + NoteColumns.MODIFIED_DATE, + // 笔记数量 + NoteColumns.NOTES_COUNT, + // 父级ID + NoteColumns.PARENT_ID, + // 摘要/片段内容 + NoteColumns.SNIPPET, + // 笔记类型 + NoteColumns.TYPE, + // 小部件ID + NoteColumns.WIDGET_ID, + // 小部件类型 + NoteColumns.WIDGET_TYPE, + // 同步ID + NoteColumns.SYNC_ID, + // 本地修改标志 + NoteColumns.LOCAL_MODIFIED, + // 原始父级ID + NoteColumns.ORIGIN_PARENT_ID, + // 与Google任务关联的ID + NoteColumns.GTASK_ID, + // 笔记的版本 + NoteColumns.VERSION + } + + + // 定义各个列的索引值,表示每个列在数据库查询结果中的位置 + + // 笔记的ID列位置 + public static final int ID_COLUMN = 0; + + // 提醒日期列位置 + public static final int ALERTED_DATE_COLUMN = 1; + + // 背景颜色ID列位置 + 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; + + // 父级ID列位置 + public static final int PARENT_ID_COLUMN = 7; + + // 笔记摘要列位置 + public static final int SNIPPET_COLUMN = 8; + + // 笔记类型列位置 + public static final int TYPE_COLUMN = 9; + + // 小部件ID列位置 + public static final int WIDGET_ID_COLUMN = 10; + + // 小部件类型列位置 + public static final int WIDGET_TYPE_COLUMN = 11; + + // 同步ID列位置 + public static final int SYNC_ID_COLUMN = 12; + + // 本地修改标志列位置 + public static final int LOCAL_MODIFIED_COLUMN = 13; + + // 原始父级ID列位置 + public static final int ORIGIN_PARENT_ID_COLUMN = 14; + + // Google任务ID列位置 + public static final int GTASK_ID_COLUMN = 15; + + // 笔记版本列位置 + public static final int VERSION_COLUMN = 16; + +// 定义该类的成员变量 + + // 应用程序的上下文,通常用来访问系统服务和资源 + private Context mContext; + + // 内容解析器,用于与系统或应用程序的数据库进行交互 + private ContentResolver mContentResolver; + + // 标识该笔记是否是新创建的,布尔值标识 + private boolean mIsCreate; + + // 笔记的唯一标识符ID + private long mId; + + // 笔记的提醒日期时间戳,表示笔记的提醒时间 + private long mAlertDate; + + // 笔记的背景颜色ID,用于设置笔记的视觉背景颜色 + private int mBgColorId; + + // 笔记的创建日期时间戳,表示笔记被创建的时间 + private long mCreatedDate; + + // 一个整数值,表示该笔记是否包含附件,通常0表示没有附件,1表示有附件 + private int mHasAttachment; + + // 笔记的最后修改日期时间戳,表示笔记被修改的时间 + private long mModifiedDate; + + // 父级笔记的ID,通常用于表示该笔记所属的父级笔记(如果存在的话) + private long mParentId; + + // 笔记的摘要或片段内容,通常是笔记的简短描述 + private String mSnippet; + + // 笔记的类型,通常是一个整数,表示笔记的类型(例如文本、图片等) + private int mType; + + // 小部件的ID,表示与笔记相关联的小部件的唯一标识符 + private int mWidgetId; + + // 小部件的类型,表示小部件的种类或用途 + private int mWidgetType; + + // 原始父级笔记的ID,用于记录笔记的初始父级ID(如果有父级笔记) + private long mOriginParent; + + // 笔记的版本号,用于表示笔记的版本,通常用于版本控制 + private long mVersion; + + // 存储笔记的差异值,通常用于记录笔记的变化(例如修改后的字段值) + private ContentValues mDiffNoteValues; + + // 一个 `SqlData` 对象的列表,通常用于存储与该笔记相关的多条数据(例如附加数据、历史记录等) + private ArrayList mDataList; + + + public SqlNote(Context context) { + + // 初始化应用程序上下文 + mContext = context; + + // 获取内容解析器,用于与应用程序的数据库交互 + mContentResolver = context.getContentResolver(); + + // 将该笔记标识为新创建的笔记 + mIsCreate = true; + + // 设置该笔记的唯一标识符ID。通常初始为一个无效 ID,用于数据库插入之前的标识符占位 + mId = INVALID_ID; + + // 默认的提醒日期时间戳为 0,表示没有设置提醒 + mAlertDate = 0; + + // 从资源中获取默认的背景颜色ID + mBgColorId = ResourceParser.getDefaultBgId(context); + + // 设置笔记的创建日期时间戳,使用当前系统时间的毫秒数 + mCreatedDate = System.currentTimeMillis(); + + // 默认没有附件 + mHasAttachment = 0; + + // 设置笔记的最后修改日期时间戳,使用当前系统时间的毫秒数 + mModifiedDate = System.currentTimeMillis(); + + // 默认的父级ID为 0,表示该笔记没有父级笔记 + mParentId = 0; + + // 笔记的摘要部分,初始化为空字符串 + mSnippet = ""; + + // 笔记类型,默认为普通笔记 + mType = Notes.TYPE_NOTE; + + // 小部件ID,默认无效值 + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + + // 小部件类型,默认为无效值 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + + // 原始父级ID,默认为 0 + mOriginParent = 0; + + // 笔记的版本,初始版本为 0 + mVersion = 0; + + // 创建一个新的 `ContentValues` 实例,用于存储笔记数据的差异 + mDiffNoteValues = new ContentValues(); + + // 初始化一个 `ArrayList`,用于存储与该笔记相关的其他数据(如附件数据、笔记历史记录等) + mDataList = new ArrayList(); + } + + + public SqlNote(Context context, Cursor c) { + + // 初始化上下文和内容解析器 + mContext = context; + + mContentResolver = context.getContentResolver(); + + // 标记当前笔记对象不是新创建的,通常从数据库查询出来的笔记对象 + mIsCreate = false; + + // 从 Cursor 中加载数据填充 SqlNote 对象 + loadFromCursor(c); + + // 初始化一个 ArrayList,用于存储笔记相关的其他数据(如附件、历史记录等) + mDataList = new ArrayList(); + + // 如果笔记类型是普通笔记(非小部件或其他类型),则加载笔记的内容 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + + // 初始化 ContentValues,用于存储笔记的变化数据 + mDiffNoteValues = new ContentValues(); + } + + + public SqlNote(Context context, long id) { + // 初始化上下文和内容解析器 + mContext = context; + mContentResolver = context.getContentResolver(); + + // 标记当前笔记对象不是新创建的 + mIsCreate = false; + + // 根据 id 从数据库中加载笔记数据 + loadFromCursor(id); + + // 初始化一个 ArrayList,用于存储笔记相关的其他数据(如附件、历史记录等) + mDataList = new ArrayList(); + + // 如果笔记类型是普通笔记(非小部件或其他类型),则加载笔记的内容 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + + // 初始化 ContentValues,用于存储笔记的变化数据 + mDiffNoteValues = new ContentValues(); + } + + // 根据提供的 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) { + + // 将 Cursor 移动到第一行(一般只有一行数据) + c.moveToNext(); + + // 从 Cursor 中加载数据到 SqlNote 对象 + loadFromCursor(c); + } else { + + // 如果查询结果为空,输出警告日志 + Log.w(TAG, "loadFromCursor: cursor = null"); + } + } finally { + + // 无论如何,确保在方法结束时关闭 Cursor + if (c != null) + c.close(); + } + } + + + private void loadFromCursor(Cursor c) { + // 从 Cursor 中提取各列的值并赋给相应的成员变量。 + // ID 列的值 + mId = c.getLong(ID_COLUMN); + + // 提醒时间列的值 + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); + + // 背景颜色 ID 列的值 + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); + + // 创建日期列的值 + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); + + // 是否包含附件的标志(0 或 1) + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); + + // 修改日期列的值 + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); + + // 父笔记 ID 列的值 + mParentId = c.getLong(PARENT_ID_COLUMN); + + // 摘要内容列的值 + mSnippet = c.getString(SNIPPET_COLUMN); + + // 笔记类型(例如:普通笔记、待办事项等) + mType = c.getInt(TYPE_COLUMN); + + // 小部件 ID 列的值 + mWidgetId = c.getInt(WIDGET_ID_COLUMN); + + // 小部件类型列的值 + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); + + // 版本号列的值 + mVersion = c.getLong(VERSION_COLUMN); + } + + private void loadDataContent() { + // 定义一个 Cursor 用于查询数据 + Cursor c = null; + + // 清空原有的数据列表,准备加载新的数据 + mDataList.clear(); + try { + // 查询与当前笔记 ID 相关的数据 + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { String.valueOf(mId) }, null); + + // 如果查询返回的 Cursor 不为空 + if (c != null) { + // 如果查询结果为空,输出警告日志 + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); + return; + } + + // 遍历查询结果,将每一行数据封装成 SqlData 对象并添加到 mDataList 中 + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); + mDataList.add(data); + } + } else { + + // 如果查询的 Cursor 为 null,输出警告日志 + Log.w(TAG, "loadDataContent: cursor = null"); + } + } finally { + + // 确保查询结束后关闭 Cursor,释放资源 + if (c != null) + c.close(); + } + } + + + public boolean setContent(JSONObject js) { + try { + // 从传入的 JSON 对象中获取与笔记相关的元数据部分 + 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) { + // 获取摘要字段的值,如果没有则设为空字符串 + 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) { + // 从传入的 JSON 中获取数据部分(如果有) + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 获取笔记的唯一 ID,如果没有则设为无效 ID + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + + // 如果是创建的笔记,或 ID 字段发生变化,则记录变化 + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, 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 字段发生变化,则记录变化 + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + // 更新当前笔记对象的背景颜色 ID + 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; + } + + + // 1. 获取修改日期,如果没有该字段则使用当前系统时间戳 + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); +// 2. 如果是创建操作,或者修改日期发生变化,则记录该字段的变化 + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } +// 3. 更新当前笔记对象的修改日期 + mModifiedDate = modifiedDate; + +// 4. 获取父级 ID(如果没有该字段则默认为 0),即记录笔记的父层级信息 + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; +// 5. 如果是创建操作,或者父级 ID 发生变化,则记录该字段的变化 + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } +// 6. 更新当前笔记对象的父级 ID + mParentId = parentId; + +// 7. 获取笔记摘要,如果没有该字段则默认为空字符串 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; +// 8. 如果是创建操作,或者摘要发生变化,则记录该字段的变化 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } +// 9. 更新当前笔记对象的摘要 + mSnippet = snippet; + +// 10. 获取笔记类型(如果没有该字段则默认为普通笔记类型) + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; +// 11. 如果是创建操作,或者笔记类型发生变化,则记录该字段的变化 + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } +// 12. 更新当前笔记对象的类型 + mType = type; + +// 13. 获取小部件 ID(如果没有该字段则使用无效值) + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; +// 14. 如果是创建操作,或者小部件 ID 发生变化,则记录该字段的变化 + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } +// 15. 更新当前笔记对象的小部件 ID + mWidgetId = widgetId; + +// 16. 获取小部件类型(如果没有该字段则默认为无效类型) + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; +// 17. 如果是创建操作,或者小部件类型发生变化,则记录该字段的变化 + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } +// 18. 更新当前笔记对象的小部件类型 + mWidgetType = widgetType; + +// 19. 获取原始父级 ID(如果没有该字段则默认为 0),通常用于记录笔记的移动历史或原始位置 + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; +// 20. 如果是创建操作,或者原始父级 ID 发生变化,则记录该字段的变化 + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } +// 21. 更新当前笔记对象的原始父级 ID + mOriginParent = originParent; + +// 22. 遍历 `dataArray` 数组中的所有数据(可能是笔记的附加数据或附件等) + for (int i = 0; i < dataArray.length(); i++) { + // 23. 获取当前的数据项(JSONObject) + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + + // 24. 如果数据项包含 ID 字段,则尝试在当前的数据列表中查找相应的 `SqlData` + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + // 25. 如果在列表中找到了对应 ID 的 `SqlData`,则赋值 + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + + // 26. 如果没有找到对应的 `SqlData`,则创建一个新的 `SqlData` 实例 + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); // 将新创建的 `SqlData` 添加到数据列表中 + } + + // 27. 将当前的 JSON 数据传入 `sqlData` 对象进行处理 + sqlData.setContent(data); + } + } + } + catch (JSONException e) { + // 1. 捕获并处理 JSON 异常(JSONException),这是针对在解析 JSON 数据过程中可能发生的异常 + // 2. 将异常信息记录到日志中,使用 Log.e 方法打印错误日志。TAG 是日志标签,通常用于标识日志来源 + Log.e(TAG, e.toString()); + + // 3. 打印堆栈跟踪信息,以便开发人员可以追踪异常发生的调用链和位置 + e.printStackTrace(); + // 4. 返回 false,表示在处理过程中发生了异常,操作失败 + return false; + } + + // 5. 如果没有异常发生,表示操作成功,返回 true + return true; + + } + + public JSONObject getContent() { + try { + // 1. 创建一个新的 JSON 对象,用于存储最终返回的数据 + JSONObject js = new JSONObject(); + + // 2. 检查是否是创建状态,如果是创建状态,直接返回 null + if (mIsCreate) { + // 记录日志,提示该项尚未创建 + Log.e(TAG, "it seems that we haven't created this in database yet"); + + // 由于没有创建记录,返回 null + return null; + } + + // 3. 创建一个用于存储 note 信息的 JSON 对象 + JSONObject note = new JSONObject(); + + // 4. 如果 mType 为 Notes.TYPE_NOTE,表示这是一个普通笔记 + if (mType == Notes.TYPE_NOTE) { + + // 将笔记的各个属性添加到 JSON 对象中 + // 添加 ID + note.put(NoteColumns.ID, mId); + // 添加提醒日期 + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + // 添加背景颜色 ID + 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); + // 添加父 ID + note.put(NoteColumns.PARENT_ID, mParentId); + // 添加笔记摘录 + note.put(NoteColumns.SNIPPET, mSnippet); + // 添加笔记类型 + note.put(NoteColumns.TYPE, mType); + // 添加小部件 ID + note.put(NoteColumns.WIDGET_ID, mWidgetId); + // 添加小部件类型 + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + // 添加原始父 ID + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + // 将 note JSON 对象添加到最终返回的 JSON 对象中 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + + // 5. 创建一个 JSON 数组,用于存储数据项 + JSONArray dataArray = new JSONArray(); + for (SqlData sqlData : mDataList) { + // 获取 sqlData 对象的内容 + JSONObject data = sqlData.getContent(); + if (data != null) { + // 将数据项添加到 JSON 数组中 + dataArray.put(data); + } + } + + // 将数据数组添加到最终返回的 JSON 对象中 + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } + + // 6. 如果 mType 是 TYPE_FOLDER 或 TYPE_SYSTEM,表示这是一个文件夹或系统类型 + else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + + // 将文件夹或系统类型的基本信息添加到 JSON 对象中 + note.put(NoteColumns.ID, mId); // 添加 ID + note.put(NoteColumns.TYPE, mType); // 添加类型 + note.put(NoteColumns.SNIPPET, mSnippet); // 添加摘录 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 将 note JSON 对象添加到最终返回的 JSON 对象中 + } + + // 7. 返回最终的 JSON 对象 + return js; + } catch (JSONException e) { + + // 8. 捕获并处理 JSON 解析时的异常 + Log.e(TAG, e.toString()); // 记录异常信息 + e.printStackTrace(); // 打印异常的堆栈跟踪信息 + } + // 如果发生异常,则返回 null + return null; + } + + public void setParentId(long id) { + // 9. 设置父 ID + // 更新实例的 mParentId 属性 + mParentId = id; + + // 将父 ID 放入 mDiffNoteValues 中,可能用于后续的数据库更新 + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } + + + public void setGtaskId(String gid) { + // 设置 Gtask ID 到 mDiffNoteValues 中 + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } + + public void setSyncId(long syncId) { + // 设置同步 ID 到 mDiffNoteValues 中 + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } + + public void resetLocalModified() { + // 重置本地修改标志,将其设置为 0 + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } + + public long getId() { + // 返回笔记的 ID + return mId; + } + + public long getParentId() { + // 返回父 ID + return mParentId; + } + + public String getSnippet() { + // 返回笔记的摘录 + return mSnippet; + } + + public boolean isNoteType() { + // 判断当前对象是否是普通笔记类型 + return mType == Notes.TYPE_NOTE; + } + + public void commit(boolean validateVersion) { + // 如果是新创建的笔记 + if (mIsCreate) { + + // 如果笔记 ID 是无效的,且 mDiffNoteValues 中包含 ID,移除它 + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + // 移除无效的 ID + mDiffNoteValues.remove(NoteColumns.ID); + } + + // 尝试将笔记插入数据库,mDiffNoteValues 存储了笔记的所有变化 + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + + // 从 URI 获取生成的 ID + // 获取 URI 路径段中的 ID 部分 + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + // 如果发生异常,记录错误并抛出自定义异常 + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + + // 如果获取到的 ID 为 0,说明创建失败,抛出异常 + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + + // 如果是笔记类型(不是文件夹或系统类型),则提交相关的 SQL 数据 + if (mType == Notes.TYPE_NOTE) { + // 对每个数据项进行提交 + for (SqlData sqlData : mDataList) { + // 提交数据项,传递笔记 ID + 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"); + } + + // 如果 mDiffNoteValues 中有更改数据 + 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) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); // 提交数据项 + } + } + } + + // 刷新本地数据 + // 从数据库中加载当前笔记的信息 + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + // 如果是笔记类型,加载相关数据内容 + loadDataContent(); + + // 清空更改的数据记录 + mDiffNoteValues.clear(); + + // 标记为非创建状态 + mIsCreate = false; + } +} \ No newline at end of file diff --git a/Task.java b/Task.java new file mode 100644 index 0000000..aa67722 --- /dev/null +++ b/Task.java @@ -0,0 +1,696 @@ +/* + * 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. + */ + +//包声明: +// 该类属于 net.micode.notes.gtask.data 包,通常表示这是一个与 Google Tasks 或某个任务管理相关的数据模型类。 +package net.micode.notes.gtask.data; + +//导入 Cursor 类: +// Cursor 是 Android 中用于从数据库中查询数据的接口,通常用于访问 SQLite 数据库中的记录。 +import android.database.Cursor; + +//导入 TextUtils 类: +// TextUtils 是 Android 提供的工具类,包含了一些常用的字符串操作方法,如判断字符串是否为空或是否匹配某些模式等。 +import android.text.TextUtils; + +//导入 Log 类: +// Log 类用于 Android 中的日志输出,开发者可以使用它记录调试信息、错误日志等。 +import android.util.Log; + +//导入 Notes 类: +// 这个类可能是应用中用于表示便签或任务的类。Notes 类中的常量和方法通常用于访问或操作任务和便签相关的数据。 +import net.micode.notes.data.Notes; + +//导入 DataColumns, DataConstants, NoteColumns: +// 这些导入语句表明,这些类包含 Notes 类中的常量和字段,它们可能用于表示与任务或便签相关的列名、常量定义等。 +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +//导入 ActionFailureException 类: +// 这个自定义异常类 ActionFailureException 可能用于处理在执行某些操作时发生的错误,表示某些操作失败。 +import net.micode.notes.gtask.exception.ActionFailureException; + +//导入 GTaskStringUtils 工具类: +// 这个类看起来是与 Google Tasks 相关的字符串工具类,可能包含处理任务相关字符串的实用方法。 +import net.micode.notes.tool.GTaskStringUtils; + +//导入 org.json 库的类:这些类用于处理 JSON 数据。 +// 在任务管理应用中,任务通常是以 JSON 格式存储或传输的,JSONArray, JSONException, 和 JSONObject 提供了处理 JSON 数据所需的工具。 +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +//类:Task 类继承自 Node 类,表示任务的基本模型,包含任务的状态、备注、父任务、优先级等信息。 +//Task 类的构造函数: +//初始化了 mCompleted(任务是否完成)、mNotes(任务备注)、mPriorSibling(前一个兄弟任务)、 +// mParent(父任务列表)和 mMetaInfo(任务的元数据信息)等属性。 +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; + + // 构造函数:初始化 Task 对象的默认值 + public Task() { + // 调用父类构造函数(假设父类 Node 也有构造函数) + super(); + // 默认任务未完成 + mCompleted = false; + // 默认没有备注 + mNotes = null; + // 默认没有前一个兄弟任务 + mPriorSibling = null; + // 默认没有父任务列表 + mParent = null; + // 默认没有元数据 + mMetaInfo = null; + } + + /** + * 生成任务创建的 JSON 数据,表示一个任务的创建动作。 + * + * @param actionId 任务创建动作的 ID + * @return 生成的包含任务创建信息的 JSONObject + */ + /* + 方法:getCreateAction(int actionId) 生成一个包含任务创建信息的 JSONObject,用于表示一个任务创建的动作。 + + getCreateAction(int actionId) 方法: + 功能: + 该方法的主要任务是生成一个 JSONObject,它表示创建任务的动作(create action)。 + 这个 JSON 对象会包含任务的基本信息,比如名称、创建者 ID、任务类型、父任务 ID、任务列表 ID 等等。 + 步骤解析: + 初始化 JSONObject:创建一个新的 JSONObject(js)对象,用于保存任务创建的所有信息。 + 设置基本属性: + action_type:表示动作类型,这里是创建任务。 + action_id:任务创建动作的唯一 ID,通过方法参数 actionId 传入。 + index:任务在其父任务列表中的索引位置,使用 mParent.getChildTaskIndex(this) 获取当前任务在父任务列表中的位置。 + 创建任务的基本信息: + 创建一个名为 entity 的 JSON 对象,包含任务的名称、创建者 ID(默认为 null)、任务类型(这里是 "task")等基本信息。 + 添加备注: + 如果任务有备注(mNotes 不为 null),则将备注信息添加到 entity 中。 + 设置父任务 ID 和任务列表 ID: + 分别通过 mParent.getGid() 获取父任务列表的 ID,并将其存储在 JSON 对象中。 + 设置前一个兄弟任务 ID: + 如果当前任务有前一个兄弟任务(mPriorSibling != null),则将其 ID 添加到 JSON 对象中。 + 错误处理: + 在构建 JSON 的过程中,如果发生 JSONException 异常,记录错误信息并抛出自定义的 ActionFailureException。 + JSONException 和 ActionFailureException: + + JSONException: + 这是 JSON 操作过程中可能抛出的异常,用于处理在构建 JSON 对象时出现的错误。 + ActionFailureException: + 这是自定义的异常类,用于表示任务创建过程中操作失败的情况。在 JSON 构建失败时,抛出该异常。 + 日志记录: + + 使用 Log.e(TAG, e.toString()) 将错误信息打印到日志中,以便开发者调试时查看。 + e.printStackTrace() 打印异常栈信息,帮助追踪错误的详细位置。 + + */ + public JSONObject getCreateAction(int actionId) { + // 创建一个新的 JSON 对象,用于保存任务的创建信息 + 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); + + // 设置任务在父任务列表中的索引位置 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // 创建一个 JSON 对象 entity,用于表示任务的基本信息(任务名称、创建者 ID、任务类型等) + JSONObject entity = new JSONObject(); + // 设置任务的名称 + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + // 设置任务创建者 ID,这里默认为 "null" + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_TASK); // 设置任务的类型为 "task" + + // 如果任务有备注信息,添加备注到 entity 中 + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + + // 将任务的 entity 信息放入 JSON 中 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + // 设置任务的父任务 ID(当前任务所在的任务列表的 ID) + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // 设置目标父级类型,这里固定为 "group" 类型 + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // 设置任务列表的 ID(父任务列表的 ID) + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // 如果当前任务有前一个兄弟任务,则设置 prior_sibling_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(); + throw new ActionFailureException("fail to generate task-create jsonobject"); + } + + // 返回生成的 JSON 对象 + return js; + } +} + + + + +/* + 方法功能: + getUpdateAction(int actionId) 生成一个 JSON 对象,表示任务的更新动作。这个 JSON 对象包含任务的 ID、名称、备注、删除标记等信息,用于更新任务时进行传输或存储。 + 参数: + actionId,表示当前任务更新动作的唯一 ID。 + 返回值: + 返回一个 JSONObject,包含任务更新的相关信息。 + + + js.put(): + 这段代码使用 js.put() 方法将任务更新的相关数据填充到 JSON 对象中。 + put() 方法是 JSONObject 类提供的一个 API,用于向 JSON 对象中添加键值对。 + 更新操作的标识: + GTaskStringUtils.GTASK_JSON_ACTION_TYPE 设置为 GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE,表示这是一个更新操作。 + action_id: + 使用 actionId 参数设置更新操作的唯一 ID。 + 任务 ID: + GTaskStringUtils.GTASK_JSON_ID 设置为 getGid(),表示当前任务的唯一 ID。getGid() 方法通常会返回任务的全局唯一标识符(ID)。 + 任务实体更新: + 创建一个名为 entity 的 JSON 对象,用于包含实际更新的任务信息: + 任务名称: + GTaskStringUtils.GTASK_JSON_NAME 使用 getName() 方法获取任务的名称。 + 备注信息: + 如果任务有备注(getNotes() != null),则将备注信息添加到 JSON 中。 + 删除标记: + GTaskStringUtils.GTASK_JSON_DELETED 使用 getDeleted() 方法来获取任务的删除标记(表示该任务是否已被标记为删除)。 + 错误处理: + 如果在生成 JSON 对象的过程中发生 JSONException 异常,会记录错误信息并抛出自定义异常 ActionFailureException。 + ActionFailureException 是一个自定义异常类,表示任务更新过程中的失败。异常信息为 "fail to generate task-update jsonobject",有助于开发者识别错误。 + 返回值: + 方法最终返回一个包含任务更新信息的 JSONObject。这个 JSON 对象可以用于后续的任务更新操作,如提交到服务器或保存到数据库。 + + */ + public JSONObject getUpdateAction(int actionId) { + // 创建一个新的 JSON 对象,表示任务更新动作 + JSONObject js = new JSONObject(); + + try { + // 设置 action_type 字段,表示这是一个更新操作 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // 设置 action_id 字段,表示当前更新操作的唯一标识符 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务的 ID,使用 `getGid()` 方法获取当前任务的唯一 ID + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // 创建一个 entity 对象,表示任务更新的实际内容(包括任务的名称、备注和删除标记) + JSONObject entity = new JSONObject(); + + // 设置任务的名称 + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + + // 如果任务有备注信息,则添加备注字段 + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + + // 设置任务的删除标记,使用 `getDeleted()` 方法获取任务是否已删除 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + + // 将 entity 对象放入 JSON 中,作为任务更新的实际内容 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + // 捕获 JSON 构建过程中的异常 + // 打印错误日志 + Log.e(TAG, e.toString()); + // 输出堆栈信息 + e.printStackTrace(); + // 如果发生异常,抛出自定义的更新失败异常 + throw new ActionFailureException("fail to generate task-update jsonobject"); + } + + // 返回生成的任务更新 JSON 对象 + return js; + } + + + /* + 这段代码展示了一个 `setContentByRemoteJSON(JSONObject js)` 方法,它用于根据从远程服务器或外部系统接收到的 JSON 数据更新任务的属性 + - **方法功能**:`setContentByRemoteJSON(JSONObject js)` 根据传入的 JSON 对象,更新当前任务的各个属性。该方法会检查 JSON 中是否包含某些字段,并根据字段的存在情况,调用相应的 setter 方法更新任务的属性值。 + - **参数**:`js`,一个 `JSONObject`,包含任务更新的信息。 + - **返回值**:无返回值。该方法直接更新任务对象的属性。 + + 1. **检查 JSON 对象是否为空**: + - 该方法首先检查传入的 `js` 对象是否为 `null`。如果是 `null`,说明没有数据需要处理,方法会直接退出。 + + 2. **处理各个属性字段**: + - 代码通过 `js.has()` 方法检查 JSON 对象中是否包含特定字段。如果字段存在,就从 JSON 中提取相应的值,并通过 setter 方法更新任务对象的属性。 + - 对应的字段名称通过 `GTaskStringUtils` 类中的常量定义,如 `GTASK_JSON_ID`、`GTASK_JSON_LAST_MODIFIED`、`GTASK_JSON_NAME` 等。这些常量在其他地方应该有定义,用于确保字段名称的一致性,避免硬编码的字符串。 + + 3. **具体字段的处理**: + - **任务 ID**:使用 `getString()` 获取任务 ID,并通过 `setGid()` 方法更新任务的全局 ID。 + - **最后修改时间**:使用 `getLong()` 获取任务的最后修改时间,并通过 `setLastModified()` 方法更新。 + - **任务名称**:使用 `getString()` 获取任务名称,并通过 `setName()` 方法更新。 + - **备注信息**:使用 `getString()` 获取任务的备注,如果备注字段存在,就更新备注信息。 + - **删除标记**:使用 `getBoolean()` 获取任务的删除标记(布尔值),并通过 `setDeleted()` 更新。 + - **完成标记**:使用 `getBoolean()` 获取任务的完成标记(布尔值),并通过 `setCompleted()` 更新。 + + 4. **异常处理**: + - 如果在从 JSON 中提取数据的过程中发生 `JSONException` 异常,会打印错误信息并抛出自定义异常 `ActionFailureException`,该异常指示任务内容从 JSON 对象中获取失败。 + - 异常消息中说明了错误是由于无法从 JSON 对象获取任务内容("fail to get task content from jsonobject")。 + + 5. **setter 方法**: + - `setGid()`, `setLastModified()`, `setName()`, `setNotes()`, `setDeleted()`, 和 `setCompleted()` 等方法是用于设置任务属性的自定义方法。 + 这些方法应该在任务对象的类中定义,用来更新任务对象的相应字段。 + - 该方法通过从传入的 JSON 对象中提取任务相关的信息(如任务 ID、最后修改时间、名称、备注、删除标记、完成标记等),并通过调用相应的 setter 方法来更新任务对象的属性。 + - 在实现时,使用了 `js.has()` 来判断是否包含某个字段,避免了因字段缺失而导致的异常。 + - 错误处理机制较为健全,当解析 JSON 过程中发生异常时,会记录日志并抛出自定义异常,以便调用者进行处理。 + + */ + + public void setContentByRemoteJSON(JSONObject js) { + // 如果传入的 JSON 对象不为 null + if (js != null) { + try { + // 处理任务 ID,获取并设置任务的全局 ID + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // 处理任务的最后修改时间,获取并设置 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // 处理任务名称,获取并设置 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + // 处理任务备注,获取并设置 + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + + // 处理删除标记,获取并设置 + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + + // 处理完成标记,获取并设置 + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + // 如果解析 JSON 过程中发生异常,打印错误信息并抛出自定义异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get task content from jsonobject"); + } + } + } + + + + + /* + 功能: + 从本地的 JSON 数据中提取相关内容,更新任务的名称。如果 JSON 格式正确且符合预期,它会提取相关字段并更新任务的 name 属性。否则,方法会打印警告信息或错误信息。 + 参数: + js,一个包含任务信息的 JSONObject,格式应包括 META_HEAD_NOTE 和 META_HEAD_DATA 这两个字段。 + 返回值: + 无返回值,直接修改任务对象的属性。 + + */ + public void setContentByLocalJSON(JSONObject js) { + // 检查传入的 JSON 是否为 null,且是否包含所需的两个字段 + /* + 这段代码首先检查传入的 js 对象是否为 null,以及它是否包含两个特定字段:META_HEAD_NOTE 和 META_HEAD_DATA。 + 这两个字段是该方法解析任务内容所依赖的。如果这两个字段不存在,或者 js 为 null,则会打印警告日志并返回,避免进一步的处理。 + */ + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) + || !js.has(GTaskStringUtils.META_HEAD_DATA)) { + // 如果条件不满足,打印警告信息并返回 + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + + /*如果 JSON 对象有效并且包含所需字段, + 接下来会从 JSON 对象中提取出 META_HEAD_NOTE 部分(作为 note 对象)和 META_HEAD_DATA 部分(作为 dataArray 数组) + */ + + // 从 JSON 中提取出 META_HEAD_NOTE 对应的 JSONObject + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 从 JSON 中提取出 META_HEAD_DATA 对应的 JSONArray + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 检查 note 对象中的 TYPE 是否为 NOTE 类型 + //通过 note.getInt(NoteColumns.TYPE) 获取 note 对象中的 TYPE 字段,并与 Notes.TYPE_NOTE 常量进行比较。 + // 如果 TYPE 不等于 Notes.TYPE_NOTE(假设 Notes.TYPE_NOTE 是预定义的常量,表示有效的 note 类型), + // 则认为该数据无效,打印错误日志并退出方法。 + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + // 如果 TYPE 不等于 NOTE 类型,打印错误信息并返回 + Log.e(TAG, "invalid type"); + return; + } + + // 遍历 dataArray 数组中的每个 JSONObject + /*方法通过 dataArray.length() 获取数组的长度,使用 for 循环遍历数组中的每一个 data 对象。 + 在每次迭代中,通过 data.getString(DataColumns.MIME_TYPE) 获取 MIME_TYPE 字段, + 并与 DataConstants.NOTE 常量进行比较。如果 MIME_TYPE 的值是 NOTE,则获取该 data 对象的 CONTENT 字段值, + 并调用 setName() 方法更新任务的名称。 + 一旦找到符合条件的 data 对象,便立即跳出循环(使用 break 语句) + */ + for (int i = 0; i < dataArray.length(); i++) { + // 获取当前遍历的 data 对象 + JSONObject data = dataArray.getJSONObject(i); + // 判断 data 中的 MIME_TYPE 是否为 NOTE 类型 + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + // 如果 MIME_TYPE 为 NOTE 类型,从 data 中获取 CONTENT 字段的值,并设置为任务的名称 + setName(data.getString(DataColumns.CONTENT)); + // 找到第一个符合条件的记录后跳出循环 + break; + } + } + + } + //如果在解析 JSON 数据时发生异常(例如字段缺失或类型不匹配),会捕获 JSONException 并打印错误信息,确保应用不会崩溃。 + catch (JSONException e) { + // 如果在解析过程中发生异常,打印错误信息 + Log.e(TAG, e.toString()); + // 打印堆栈跟踪信息 + e.printStackTrace(); + } + } + + + + /* + 该方法 getLocalJSONFromContent() 的主要功能是根据当前任务的状态生成或更新本地的 JSON 对象(JSONObject), + 该对象表示任务的元数据。它会根据任务是否已经同步(即是否有 mMetaInfo)来决定是创建一个新的 JSON 对象,还是更新已有的同步任务的数据。 + + */ + public JSONObject getLocalJSONFromContent() { + // 获取当前任务的名称 + String name = getName(); + try { + // 如果 mMetaInfo 为 null,表示任务还未同步,或者是一个新任务 + if (mMetaInfo == null) { + // new task created from web + + // 如果任务的名称为空,输出警告并返回 null + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; + } + + // 创建一个新的 JSON 对象来表示任务的元数据 + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + + // 将任务的名称添加到 data 对象中 + data.put(DataColumns.CONTENT, name); + // 将 data 对象添加到 dataArray 中 + dataArray.put(data); + + // 将 dataArray 添加到 js 对象中的 META_HEAD_DATA 字段 + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + + // 设置 note 对象的 TYPE 字段为 NOTE 类型 + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + // 将 note 对象添加到 js 对象中的 META_HEAD_NOTE 字段 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + + return js; // 返回生成的 JSON 对象 + + } else { + + // 如果 mMetaInfo 已经存在,表示这是一个已经同步的任务 + // 从 mMetaInfo 中获取 META_HEAD_NOTE 和 META_HEAD_DATA + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 遍历 dataArray 中的每个 data 对象,更新 MIME_TYPE 为 NOTE 的对象中的 CONTENT 字段 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + // 更新 data 对象中的 CONTENT 字段为当前任务的名称 + data.put(DataColumns.CONTENT, getName()); + break; // 找到并更新后跳出循环 + } + } + + // 确保 note 对象中的 TYPE 字段仍然为 NOTE 类型 + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + // 返回已经更新的 mMetaInfo 对象 + return mMetaInfo; + } + } + // 捕获并处理 JSON 解析过程中的异常 + catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + // 如果发生异常,返回 null + return null; + } + } + + + + /* + + setMetaInfo(MetaData metaData): + 该方法接受一个 MetaData 对象作为参数,目的是将 metaData 中的 notes 信息转换为 JSONObject,并赋值给 mMetaInfo。 + 如果传入的 metaData 和 metaData.getNotes() 不为 null,会尝试将 getNotes() 返回的字符串解析为 JSONObject,并赋值给 mMetaInfo。 + 如果解析过程中发生错误(如 JSON 格式不合法),会捕获 JSONException,输出警告信息,并将 mMetaInfo 置为 null。 + */ + public void setMetaInfo(MetaData metaData) { + if (metaData != null && metaData.getNotes() != null) { + try { + // 将 notes 字符串转换为 JSONObject + mMetaInfo = new JSONObject(metaData.getNotes()); + } + + catch (JSONException e) { // 如果 JSON 转换失败 + // 输出警告日志 + Log.w(TAG, e.toString()); + // 设置 mMetaInfo 为 null + mMetaInfo = null; + } + } + } + + + /* + getSyncAction(Cursor c): + 该方法根据当前任务的同步状态,决定接下来的同步操作(如更新远程或本地数据等)。 + 逻辑说明: + 该方法首先检查 mMetaInfo 是否包含有效的 note 元数据。 + 然后,它根据当前数据库记录(通过 Cursor 提供的数据)与 mMetaInfo 中的 note 数据进行比较,确定要执行的同步操作: + SYNC_ACTION_UPDATE_REMOTE:从远程更新数据。 + SYNC_ACTION_UPDATE_LOCAL:将远程数据应用到本地。 + SYNC_ACTION_NONE:本地和远程都没有更新,不需要同步。 + SYNC_ACTION_UPDATE_CONFLICT:本地和远程都进行了修改,发生冲突。 + SYNC_ACTION_ERROR:出现错误或无法同步。 + */ + + public int getSyncAction(Cursor c) { + try { + JSONObject noteInfo = null; + if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { + // 获取 note 元数据 + noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + } + + // 如果没有获取到 note 信息 + if (noteInfo == null) { + Log.w(TAG, "it seems that note meta has been deleted"); + // 需要从远程更新 + return SYNC_ACTION_UPDATE_REMOTE; + } + + // 如果 note 缺少 ID 字段 + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + // 需要从本地更新 + return SYNC_ACTION_UPDATE_LOCAL; + } + + // 校验 note 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) { + // 本地和远程都没有变化 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 无需操作 + return SYNC_ACTION_NONE; + } else { // 远程有更新 + // 从远程更新 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { // 本地已修改 + // 校验 gtask ID + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + // gtask ID 不匹配,返回错误 + return SYNC_ACTION_ERROR; + } + // 本地修改且没有远程修改 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 将本地修改同步到远程 + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // 本地和远程都有修改,发生冲突 + return SYNC_ACTION_UPDATE_CONFLICT; + } + } + } catch (Exception e) { + // 异常处理 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + // 出现异常或无法处理时返回错误 + return SYNC_ACTION_ERROR; + } + + + /* + setCompleted(boolean completed) + 作用:设置任务的完成状态。 + 参数:completed 是一个布尔值,表示任务是否已完成。 + */ + public void setCompleted(boolean completed) { + //将传入的 completed 参数赋值给任务对象的 mCompleted 属性,用于标识任务是否完成 + this.mCompleted = completed; + } + + + /* + setNotes(String notes) + 作用:设置任务的备注信息。 + 参数:notes 是一个字符串,表示任务的备注或附加说明。 + */ + public void setNotes(String notes) { + //将传入的 notes 参数赋值给任务对象的 mNotes 属性,用于存储任务的备注内容。 + this.mNotes = notes; + } + + /* + setPriorSibling(Task priorSibling) + 作用:设置任务的前一个兄弟任务。 + 参数:priorSibling 是一个 Task 对象,表示当前任务的前一个兄弟任务。 + */ + public void setPriorSibling(Task priorSibling) { + //将传入的 priorSibling 参数赋值给任务对象的 mPriorSibling 属性,表示当前任务在任务列表中的前一个任务。 + this.mPriorSibling = priorSibling; + } + + + /* + setParent(TaskList parent) + 作用:设置任务的父任务列表。 + 参数:parent 是一个 TaskList 对象,表示任务所属的任务列表。 + */ + public void setParent(TaskList parent) { + //将传入的 parent 参数赋值给任务对象的 mParent 属性,表示当前任务所属的父任务列表。 + this.mParent = parent; + } + + + /* + getCompleted() + 作用:获取任务的完成状态。 + 返回值:返回一个布尔值,表示任务是否已完成。 + */ + public boolean getCompleted() { + //返回 mCompleted 属性的值,表示任务是否已完成。 + return this.mCompleted; + } + + + /* + getNotes() + + 作用:获取任务的备注信息。 + 返回值:返回任务的备注字符串。 + */ + + public String getNotes() { + //返回 mNotes 属性的值,表示任务的备注或附加说明。 + return this.mNotes; + } + + /* + getPriorSibling() + 作用:获取任务的前一个兄弟任务。 + 返回值:返回一个 Task 对象,表示当前任务的前一个兄弟任务。 + */ + + public Task getPriorSibling() { + //返回 mPriorSibling 属性的值,表示当前任务在任务列表中的前一个任务。 + return this.mPriorSibling; + } + + + /* + getParent() + 作用:获取任务所属的父任务列表。 + 返回值:返回一个 TaskList 对象,表示任务所属的父任务列表。 + */ + + public TaskList getParent() { + // 返回 mParent 属性的值,表示当前任务所属的父任务列表。 + return this.mParent; + } + + +} diff --git a/TaskList.java b/TaskList.java new file mode 100644 index 0000000..0e4be38 --- /dev/null +++ b/TaskList.java @@ -0,0 +1,747 @@ +/* + * 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; + +// 引入 Android 数据库操作类,Cursor 用于处理数据库查询结果 +import android.database.Cursor; + +// 引入 Android 的日志工具类 +import android.util.Log; + +// 引入自定义的 Notes 数据模型,特别是 NoteColumns 类,包含笔记相关的列名 +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +// 引入 GTask 的异常处理类,用于处理 GTask 相关操作失败的情况 +import net.micode.notes.gtask.exception.ActionFailureException; + +// 引入 GTask 的字符串工具类,可能提供与 GTask 数据同步的字符串处理功能 +import net.micode.notes.tool.GTaskStringUtils; + +// 引入 JSON 处理库,处理 JSON 格式的数据 +import org.json.JSONException; +import org.json.JSONObject; + +// 引入 ArrayList 类,用于存储动态大小的列表 +import java.util.ArrayList; + + + +/* +private static final String TAG = TaskList.class.getSimpleName();: +这是用于日志记录的 TAG,便于在调试时打印日志信息时标明是哪个类生成的日志。getSimpleName() 返回类名 TaskList。 + + +private int mIndex;: +mIndex 用于表示任务列表的索引,可能用于区分不同的任务列表。 +这个索引在构造函数中初始化为 1。 + + +private ArrayList mChildren;: +mChildren 是一个存储任务对象的 ArrayList, +代表当前任务列表下的所有子任务。它是一个动态大小的列表,可以随时添加任务。 + + +public TaskList(): +这是 TaskList 类的构造函数。在构造函数中,调用了父类 Node 的构造函数(super()), +并初始化了 mChildren(一个新的空 ArrayList) +和 mIndex(设置为 1)。*/ +public class TaskList extends Node { + // 用于日志记录的 TAG,便于调试时查看输出 + private static final String TAG = TaskList.class.getSimpleName(); + + // 任务列表的索引,用于区分不同的任务列表 + private int mIndex; + + // 存储任务列表中所有任务对象的 ArrayList + private ArrayList mChildren; + + // 构造函数,初始化任务列表对象 + public TaskList() { + + // 调用父类 Node 的构造函数 + super(); + // 初始化任务列表 + mChildren = new ArrayList(); + + // 设置默认索引为 1 + mIndex = 1; + } + + +/* + +public JSONObject getCreateAction(int actionId): +该方法用于生成一个 JSON 对象,表示创建任务列表的动作。生成的 JSON 数据将用于向 GTask 服务发送创建任务列表的请求。 +参数 actionId 是一个唯一标识符,表示该创建动作的 ID。 + + +JSONObject js = new JSONObject();: +创建一个空的 JSONObject,用于存放任务列表创建请求的相关信息。 + + +js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);: +设置 JSON 中的 action_type 字段为 "create",表示这是一个创建任务列表的操作。 + + +js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);: +设置 JSON 中的 action_id 字段为传入的 actionId,这个 ID 用来唯一标识此次创建任务列表的动作。 + + +js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);: +设置 JSON 中的 index 字段为 mIndex,任务列表的索引,用于区分不同的任务列表。 + + +JSONObject entity = new JSONObject();: +创建一个新的 JSONObject,用于存放任务列表的详细信息。 + + +entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());: +设置任务列表的名称。getName() 是 Node 类的方法,可能返回当前任务列表的名称。 + + +entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");: +设置任务列表的创建者 ID。此处使用 "null" 字符串表示没有指定创建者的情况。 + + +entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, GTaskStringUtils.GTASK_JSON_TYPE_GROUP);: +设置任务列表的实体类型为 "group",表示它是一个任务组。 + + +js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);: +将包含任务列表信息的 entity JSON 对象加入到主 JSON 对象 js 中,键为 entity_delta。 + + +异常处理: +如果在生成 JSON 数据过程中遇到 JSONException 异常,捕获异常并输出错误日志,打印异常堆栈信息。 +随后抛出一个自定义的 ActionFailureException 异常,表示任务列表创建失败。 + +return js;: +返回生成的 JSON 对象,该对象包含了用于创建任务列表的所有必要信息。 +*/ + + + + /** + * 生成创建任务列表的 JSON 对象,用于请求创建新任务列表 + * + * @param actionId 唯一的动作 ID,用于标识这个请求 + * @return JSONObject 创建任务列表的 JSON 数据 + */ + + + public JSONObject getCreateAction(int actionId) { + + // 创建一个新的 JSON 对象,用于存储创建任务列表的相关数据 + JSONObject js = new JSONObject(); + + try { + + // 设置 action_type 为 "create",表示这是一个创建任务列表的动作 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // 设置 action_id,表示这次创建任务的唯一标识符 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务列表的索引,这个索引用于区分任务列表 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // 创建一个子 JSON 对象,表示任务实体的变动信息 + JSONObject entity = new JSONObject(); + + // 设置任务列表的名称 + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + + // 设置任务列表的创建者 ID,这里使用 "null" 字符串表示未知或没有指定创建者 + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + + // 设置任务实体的类型,这里是任务列表组的类型 + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // 将任务实体信息放入 JSON 对象中 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + + // 捕获 JSON 异常,打印错误日志并抛出自定义异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); + } + + // 返回生成的 JSON 对象 + return js; + } +} + + + + /*js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);: + 该行设置 JSON 中的 action_type 字段为 "update",表示这是一个更新任务列表的操作。 + + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);: + 设置 action_id 字段为传入的 actionId,用于唯一标识此次更新操作。 + + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());: + 设置任务列表的唯一标识符 id。getGid() 是获取当前任务列表的标识符的方法。 + + JSONObject entity = new JSONObject();: + 创建一个新的 JSONObject,用于存放任务实体的变动信息。 + + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());: + 将任务列表的名称加入 entity JSON 对象。getName() 返回当前任务列表的名称。 + + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());: + 将任务列表的删除状态加入 entity JSON 对象。getDeleted() 返回当前任务列表是否被删除的状态。 + + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);: + 将包含任务实体变动信息的 entity 对象放入主 JSON 对象 js 中,键为 entity_delta。 + + 异常处理: + 如果在生成 JSON 数据过程中遇到 JSONException 异常,捕获该异常并打印错误日志。之后,抛出一个自定义的 ActionFailureException 异常,表示任务列表更新失败。 + + 返回 JSON 对象: + + 最终返回构建好的 JSON 对象 js,该对象包含了任务列表的更新操作的所有必要信息。*/ + public JSONObject getUpdateAction(int actionId) { + // 创建一个空的 JSON 对象,用于存放更新请求的数据 + JSONObject js = new JSONObject(); + + try { + // 设置 JSON 中的 action_type 字段,表示这是一个更新操作 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // 设置 JSON 中的 action_id 字段,表示此次更新操作的唯一标识符 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务列表的唯一标识符 id + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // 创建一个新的 JSON 对象,表示任务实体的变动信息(entity_delta) + JSONObject entity = new JSONObject(); + + // 设置任务列表的名称,使用当前对象的 `getName()` 方法获取任务列表名称 + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + + // 设置任务列表的删除状态,使用当前对象的 `getDeleted()` 方法获取删除状态 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + + // 将包含任务实体信息的 `entity` 对象放入主 JSON 对象中 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + + // 捕获 JSON 处理过程中的异常,打印错误信息并抛出自定义的 ActionFailureException + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); + } + + // 返回生成的 JSON 对象,包含了更新任务列表所需的所有信息 + return js; + } + + + + /* if (js != null): + 该行确保传入的 JSON 对象 js 不为空。若为空则不会继续执行更新操作。 + + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); }: + 检查 JSON 对象中是否包含 id 字段。如果包含,则调用 setGid() 方法将任务列表的 ID 设置为 JSON 中的 ID。 + + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); }: + 检查 JSON 对象中是否包含 last_modified 字段。如果包含,则调用 setLastModified() 方法将任务列表的最后修改时间设置为 JSON 中的 last_modified 时间。 + + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); }: + 检查 JSON 对象中是否包含 name 字段。如果包含,则调用 setName() 方法将任务列表的名称设置为 JSON 中的 name。 + + 异常处理: + 如果在获取 JSON 数据过程中发生 JSONException 异常,捕获该异常并打印错误日志,随后抛出自定义异常 ActionFailureException,表示从 JSON 中获取任务列表内容失败。 + + 无返回值: + 该方法没有返回值,直接通过 setGid、setLastModified 和 setName 等方法更新当前对象的属性。*/ + +public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { // 确保传入的 JSON 对象不为空 + try { + // 如果 JSON 中包含任务列表的唯一标识符 (id),则更新当前对象的 GID + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // 如果 JSON 中包含任务列表的最后修改时间 (last_modified),则更新当前对象的最后修改时间 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // 如果 JSON 中包含任务列表的名称 (name),则更新当前对象的名称 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + } catch (JSONException e) { + // 捕获 JSON 处理过程中的异常,打印错误信息并抛出自定义的 ActionFailureException + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get tasklist content from jsonobject"); + } + } + } + + + + + /*创建 JSON 对象: + 创建两个 JSONObject,一个用来存放最终结果 js,另一个用来存放文件夹相关信息 folder。 + + + 获取文件夹名称: + 调用 getName() 方法获取当前任务列表的名称 folderName。 + + 处理 MIUI 文件夹前缀: + 如果任务列表的名称以 MIUI 文件夹的前缀(MIUI_FOLDER_PREFFIX)开始,去除该前缀,得到文件夹的实际名称。 + + 设置文件夹的 SNIPPET 字段: + 将 folderName 放入 folder JSON 对象中的 "SNIPPET" 字段。 + + 判断文件夹类型: + 如果 folderName 等于默认文件夹或电话笔记文件夹,则将文件夹类型设置为系统类型 Notes.TYPE_SYSTEM。 + 否则,文件夹类型设置为普通文件夹 Notes.TYPE_FOLDER。 + + 构建最终 JSON: + 将 folder 对象放入 js 对象中,键为 META_HEAD_NOTE,这是最终返回的 JSON 对象。 + + 异常处理: + 如果在生成 JSON 数据时遇到 JSONException 异常,打印错误日志并返回 null。*/ + public JSONObject getLocalJSONFromContent() { + try { + // 创建一个空的 JSON 对象,用于存放结果数据 + JSONObject js = new JSONObject(); + // 创建一个空的 JSON 对象,用于存放文件夹信息 + JSONObject folder = new JSONObject(); + + // 获取当前任务列表的名称 + String folderName = getName(); + + // 如果任务列表名称以 MIUI 文件夹前缀开头,则去除前缀 + if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) + folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), + folderName.length()); + + // 将任务列表名称作为文件夹的“SNIPPET”字段放入 folder 对象中 + 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); + + // 将 folder 对象放入主 JSON 对象中,键为 "META_HEAD_NOTE" + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + + // 返回构建好的 JSON 对象 + return js; + } catch (JSONException e) { + // 捕获 JSON 处理中的异常,打印错误日志并返回 null + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + + + /*判断本地是否有修改: + + 检查数据库中 LOCAL_MODIFIED_COLUMN 字段的值。如果该值为 0,表示本地没有修改;否则表示有本地修改。 + + + 没有本地修改的情况: + 如果本地没有修改(LOCAL_MODIFIED_COLUMN == 0),则进一步判断 SYNC_ID_COLUMN(同步 ID)是否等于 + 当前任务列表的最后修改时间 getLastModified(): + 如果两者相同,说明本地和远程都没有任何修改,返回 SYNC_ACTION_NONE,表示不需要同步。 + 如果不同,则说明远程有修改,需要将远程的数据同步到本地,返回 SYNC_ACTION_UPDATE_LOCAL。 + + 有本地修改的情况: + 如果本地有修改(LOCAL_MODIFIED_COLUMN != 0),首先检查数据库中的 GTask ID (GTASK_ID_COLUMN) 是否与 + 当前任务列表的 GTask ID 匹配: + 如果不匹配,打印错误日志并返回 SYNC_ACTION_ERROR,表示发生了 ID 不匹配的错误。 + 如果 GTask ID 匹配,继续判断同步 ID 是否与本地修改时间一致: + + 如果同步 ID 与最后修改时间一致,说明只有本地修改需要同步到远程,返回 SYNC_ACTION_UPDATE_REMOTE。 + 如果不同,说明存在文件夹冲突的情况,仍然返回 SYNC_ACTION_UPDATE_REMOTE,表示选择应用本地修改。 + 异常处理: + + 如果方法执行过程中遇到任何异常,捕获并打印日志,最后返回 SYNC_ACTION_ERROR 表示同步失败。*/ + + + public int getSyncAction(Cursor c) { + try { + // 如果数据库中没有本地修改(LOCAL_MODIFIED_COLUMN == 0) + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + + // 如果本地和远程的同步 ID 相同(没有更新) + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + + // 本地和远程都没有更新,不需要同步 + return SYNC_ACTION_NONE; + } else { + + // 远程数据有更新,需要将远程数据同步到本地 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // 如果有本地修改 + // 检查 GTask ID 是否匹配 + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + + // 如果 GTask ID 不匹配,返回错误 + return SYNC_ACTION_ERROR; + } + // 如果同步 ID 与本地修改时间相同,说明只有本地修改 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + + // 本地修改需要更新远程数据 + return SYNC_ACTION_UPDATE_REMOTE; + } else { + + // 对于文件夹冲突的情况,选择应用本地修改 + return SYNC_ACTION_UPDATE_REMOTE; + } + } + } catch (Exception e) { + + // 捕获任何异常,打印日志并返回错误 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + // 默认返回错误,表示同步操作失败 + return SYNC_ACTION_ERROR; + } + + + // mChildren 是一个存储子任务的集合(通常是 List 类型),该方法返回其当前大小(即子任务的数量)。 + public int getChildTaskCount() { + // 返回子任务的数量,即 mChildren 列表的元素数量 + return mChildren.size(); + } + + + + + /*任务验证: + 首先检查 task 是否为空,并且检查该任务是否已经存在于 mChildren 列表中。如果任务为空或已经存在,则不会执行后续的添加操作。 + + 添加任务: + 调用 mChildren.add(task) 尝试将任务添加到 mChildren 列表中,ret 用来记录添加操作是否成功。 + + 更新兄弟任务和父任务: + 设置新任务的 前置兄弟任务(priorSibling): + 如果 mChildren 不为空,设置当前任务的前置兄弟为当前 mChildren 列表的最后一个任务。 + + 设置新任务的 父任务(parent): + 将当前任务作为父任务传给新任务。 + + 返回值: + 返回任务是否成功添加到子任务列表的结果(ret)。 + */ + public boolean addChildTask(Task task) { + boolean ret = false; + // 确保任务不为空且尚未添加到子任务列表中 + if (task != null && !mChildren.contains(task)) { + // 添加任务到子任务列表 + ret = mChildren.add(task); + if (ret) { + + // 如果添加成功,需要设置该任务的 priorSibling(前置兄弟任务)和 parent(父任务) + + // 设置前置兄弟任务 + task.setPriorSibling(mChildren.isEmpty() ? null : mChildren.get(mChildren.size() - 1)); + // 设置当前任务作为任务的父任务 + task.setParent(this); + } + } + // 返回添加任务是否成功 + return ret; + } + + + + /* + 索引合法性检查: + 首先检查传入的 index 是否在有效范围内(即不小于 0 且不大于 mChildren 列表的大小)。 + 如果索引不合法,打印错误日志并返回 false,表示添加失败。 + + 任务位置检查: + 查找 task 在当前子任务列表 mChildren 中的位置。如果 task 不在列表中(即 pos == -1),则继续进行添加操作。 + + 任务添加: + 调用 mChildren.add(index, task) 在指定的索引位置将任务添加到子任务列表中。 + + 更新兄弟任务关系: + 获取新任务的前置任务: + 如果索引不为 0,说明添加位置前面有任务,需要更新新任务的 前置兄弟任务。 + 获取新任务的后置任务: + 如果索引不是列表的最后一个位置,则需要获取 后置任务 并更新其前置兄弟为当前任务。 + 返回值:方法成功执行时返回 true,表示任务已被成功添加到子任务列表。 +*/ + + public boolean addChildTask(Task task, int index) { + // 检查索引是否合法 + if (index < 0 || index > mChildren.size()) { + // 如果索引不合法,打印错误日志 + Log.e(TAG, "add child task: invalid index"); + // 返回 false,表示添加失败 + return false; + } + + // 查找任务在当前子任务列表中的位置 + int pos = mChildren.indexOf(task); + // 确保任务不为空,并且任务不在列表中 + if (task != null && pos == -1) { + + // 在指定索引位置添加任务 + mChildren.add(index, task); + + // 更新前置任务和后置任务 + Task preTask = null; + Task afterTask = null; + + // 如果索引不是 0,则获取前一个任务 + 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); + } + + // 返回 true,表示任务成功添加 + return true; + } + + + + /* + 任务索引: + 使用 mChildren.indexOf(task) 查找指定任务在子任务列表中的位置。 + 任务移除: + 如果任务存在,则调用 mChildren.remove(task) 移除该任务。 + 更新任务关系: + 将移除任务的 priorSibling 和 parent 设置为 null,表示该任务不再有前置兄弟任务和父任务。 + 如果移除的任务不是列表中的最后一个任务,需要更新后续任务的 priorSibling,使其指向前一个任务。 + 返回值: + 如果移除任务成功,返回 true,否则返回 false。 + + */ + public boolean removeChildTask(Task task) { + boolean ret = false; + // 查找任务在子任务列表中的索引 + int index = mChildren.indexOf(task); + // 如果任务存在于子任务列表中 + if (index != -1) { + // 移除该任务 + ret = mChildren.remove(task); + + // 如果移除成功 + if (ret) { + // 重置任务的 priorSibling(前置兄弟任务)和 parent(父任务) + task.setPriorSibling(null); + task.setParent(null); + + // 更新任务列表中的其他任务 + + // 如果移除的任务不是最后一个 + if (index != mChildren.size()) { + mChildren.get(index).setPriorSibling( + // 更新后续任务的前置兄弟任务 + index == 0 ? null : mChildren.get(index - 1)); + } + } + } + + // 返回任务是否成功移除 + return ret; + } + + + + /* + 索引有效性检查: + 首先检查传入的目标索引是否在有效范围内。 + + 任务存在性检查: + 使用 mChildren.indexOf(task) 确保要移动的任务存在于列表中。 + + 任务已经在目标位置: + 如果任务已经在目标位置,直接返回 true,无需执行任何操作。 + + 任务移动: + 如果任务不在目标位置,则通过调用 removeChildTask(task) 移除任务,然后调用 addChildTask(task, index) 将其添加到新的位置。 + + 返回值: + 如果任务成功移动,返回 true;否则返回 false。 + + */ + public boolean moveChildTask(Task task, int index) { + + // 检查索引是否有效 + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "move child task: invalid index"); + // 如果索引无效,打印错误日志并返回 false + return false; + } + + // 查找任务在子任务列表中的位置 + int pos = mChildren.indexOf(task); + // 如果任务不在列表中 + if (pos == -1) { + Log.e(TAG, "move child task: the task should in the list"); + // 返回 false + return false; + } + + // 如果任务已经在目标位置 + if (pos == index) + // 不需要移动,直接返回 true + return true; + + // 移除任务并将其添加到新的索引位置 + + // 调用 removeChildTask 和 addChildTask 方法进行移动 + return (removeChildTask(task) && addChildTask(task, index)); + } + + /* + 遍历子任务列表: + 使用 for 循环遍历 mChildren 列表。 + 任务匹配: + 通过调用 t.getGid().equals(gid) 检查每个任务的 gid 是否与指定的 gid 匹配。 + 返回值: + 如果找到匹配的任务,则返回该任务;否则返回 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; + } + + + /* + 返回任务索引: + 直接调用 mChildren.indexOf(task) 获取任务在子任务列表中的索引。 + + */ + public int getChildTaskIndex(Task task) { + // 返回任务在子任务列表中的索引 + return mChildren.indexOf(task); + } + + + /* + 索引有效性检查: + 首先检查传入的索引是否在有效范围内。 + 返回任务: + 如果索引有效,则通过 mChildren.get(index) 返回对应的任务,否则返回 null。 + + */ + public Task getChildTaskByIndex(int index) { + // 检查索引是否有效 + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "getTaskByIndex: invalid index"); + // 如果索引无效,返回 null + return null; + } + // 返回指定索引位置的任务 + return mChildren.get(index); + } + + + /* + 遍历子任务列表: + 使用 for-each 循环遍历 mChildren 列表。 + 任务匹配: + 检查任务的 gid 是否与指定的 gid 匹配。 + 返回值: + 如果找到匹配的任务,则返回该任务;否则返回 null。 + + */ + +public Task getChilTaskByGid(String gid) { + + // 遍历子任务列表 + for (Task task : mChildren) { + // 如果任务的 gid 匹配 + if (task.getGid().equals(gid)) + // 返回匹配的任务 + return task; + } + + // 如果没有找到,返回 null + return null; + } + + + //返回子任务列表:直接返回存储子任务的 mChildren 列表。 + + public ArrayList getChildTaskList() { + // 返回子任务列表 + return this.mChildren; + } + + + + //设置索引:将传入的 index 值设置为当前任务的索引。 + public void setIndex(int index) { + // 设置当前任务的索引 + this.mIndex = index; + } + + + + //返回索引:返回当前任务的 mIndex 值。 +public int getIndex() { + // 返回当前任务的索引 + return this.mIndex; + } + +}