|
|
/*
|
|
|
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
|
|
|
*
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
* You may obtain a copy of the License at
|
|
|
*
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
*
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
* See the License for the specific language governing permissions and
|
|
|
* limitations under the License.
|
|
|
*/
|
|
|
|
|
|
package net.micode.notes.gtask.remote;
|
|
|
|
|
|
import android.app.Activity;
|
|
|
import android.content.ContentResolver;
|
|
|
import android.content.ContentUris;
|
|
|
import android.content.ContentValues;
|
|
|
import android.content.Context;
|
|
|
import android.database.Cursor;
|
|
|
import android.util.Log;
|
|
|
|
|
|
import net.micode.notes.R;
|
|
|
import net.micode.notes.data.Notes;
|
|
|
import net.micode.notes.data.Notes.DataColumns;
|
|
|
import net.micode.notes.data.Notes.NoteColumns;
|
|
|
import net.micode.notes.gtask.data.MetaData;
|
|
|
import net.micode.notes.gtask.data.Node;
|
|
|
import net.micode.notes.gtask.data.SqlNote;
|
|
|
import net.micode.notes.gtask.data.Task;
|
|
|
import net.micode.notes.gtask.data.TaskList;
|
|
|
import net.micode.notes.gtask.exception.ActionFailureException;
|
|
|
import net.micode.notes.gtask.exception.NetworkFailureException;
|
|
|
import net.micode.notes.tool.DataUtils;
|
|
|
import net.micode.notes.tool.GTaskStringUtils;
|
|
|
|
|
|
import org.json.JSONArray;
|
|
|
import org.json.JSONException;
|
|
|
import org.json.JSONObject;
|
|
|
|
|
|
import java.util.HashMap;
|
|
|
import java.util.HashSet;
|
|
|
import java.util.Iterator;
|
|
|
import java.util.Map;
|
|
|
|
|
|
// GTaskManager类主要负责管理与Google Tasks服务的同步操作以及本地数据和远程数据之间的协调更新等工作,
|
|
|
// 采用单例模式确保整个应用中只有一个实例在运行,内部包含了诸多与同步流程相关的方法,如登录、初始化任务列表、同步内容、处理不同同步类型的操作等。
|
|
|
public class GTaskManager {
|
|
|
// 用于在日志输出中标识该类的标签,取类的简单名称,方便在日志里区分该类相关的记录
|
|
|
private static final String TAG = GTaskManager.class.getSimpleName();
|
|
|
|
|
|
// 表示同步操作成功的状态码,用于在同步方法返回结果等场景下表示同步顺利完成。
|
|
|
public static final int STATE_SUCCESS = 0;
|
|
|
// 表示同步操作出现网络错误的状态码,当在与Google Tasks服务通信过程中发生网络相关问题时返回该状态码。
|
|
|
public static final int STATE_NETWORK_ERROR = 1;
|
|
|
// 表示同步操作出现内部错误(如数据处理、JSON解析等非网络方面的错误)的状态码,用于在同步流程内部逻辑出现问题时返回。
|
|
|
public static final int STATE_INTERNAL_ERROR = 2;
|
|
|
// 表示同步操作正在进行中的状态码,用于判断是否已有同步任务在执行,避免重复发起同步操作。
|
|
|
public static final int STATE_SYNC_IN_PROGRESS = 3;
|
|
|
// 表示同步操作被取消的状态码,当外部调用取消同步方法后,同步流程结束时返回该状态码表示操作被取消的情况。
|
|
|
public static final int STATE_SYNC_CANCELLED = 4;
|
|
|
|
|
|
// 单例模式下的唯一实例对象,初始化为null,通过静态方法getInstance获取实例,确保整个应用中只有一个GTaskManager实例在运行。
|
|
|
private static GTaskManager mInstance = null;
|
|
|
// 存储当前相关的Activity对象,用于获取认证令牌等与Activity上下文相关的操作,可通过setActivityContext方法进行设置。
|
|
|
private Activity mActivity;
|
|
|
// 存储应用的上下文对象,用于获取内容解析器(ContentResolver)等操作,在同步操作等过程中频繁使用来访问本地数据库等资源。
|
|
|
private Context mContext;
|
|
|
// 用于操作本地内容提供器(Content Provider)的对象,通过上下文获取,负责对本地数据库进行查询、更新等操作,例如查询本地笔记、文件夹等数据信息。
|
|
|
private ContentResolver mContentResolver;
|
|
|
// 标记当前是否正在进行同步操作,初始化为false,在同步操作开始时设置为true,结束时设置为false,用于避免并发同步以及判断同步状态。
|
|
|
private boolean mSyncing;
|
|
|
// 标记当前同步操作是否已被取消,初始化为false,当外部调用cancelSync方法时设置为true,同步流程中会根据此标记来及时停止相关操作。
|
|
|
private boolean mCancelled;
|
|
|
// 用于存储从Google Tasks服务获取的任务列表(TaskList类型)信息,以任务列表的唯一标识符(Gid)为键,对应的TaskList对象为值,方便快速查找和管理任务列表数据。
|
|
|
private HashMap<String, TaskList> mGTaskListHashMap;
|
|
|
// 用于存储从Google Tasks服务获取的各种节点(Node类型,包括任务、任务列表等)信息,以节点的唯一标识符(Gid)为键,对应的Node对象为值,用于统一管理和操作远程节点数据。
|
|
|
private HashMap<String, Node> mGTaskHashMap;
|
|
|
// 用于存储元数据(MetaData类型)信息,以相关的唯一标识符(可能与任务等的Gid关联)为键,对应的MetaData对象为值,在同步过程中处理和维护元数据相关内容。
|
|
|
private HashMap<String, MetaData> mMetaHashMap;
|
|
|
// 存储元数据对应的任务列表对象,用于集中管理元数据相关的任务列表操作,例如加载元数据、添加元数据对应的任务等操作都围绕此对象进行。
|
|
|
private TaskList mMetaList;
|
|
|
// 用于记录本地已删除的笔记对应的ID集合,在同步过程中标记哪些本地数据已被删除,方便后续与远程数据进行对比和清理操作。
|
|
|
private HashSet<Long> mLocalDeleteIdMap;
|
|
|
// 用于建立远程节点的唯一标识符(Gid)与本地节点的ID(Nid,通常对应本地数据库中的记录ID)之间的映射关系,方便在同步时查找和关联远程与本地的数据。
|
|
|
private HashMap<String, Long> mGidToNid;
|
|
|
// 与mGidToNid相反,建立本地节点的ID(Nid)与远程节点的唯一标识符(Gid)之间的映射关系,同样用于同步操作中的数据关联和查找。
|
|
|
private HashMap<Long, String> mNidToGid;
|
|
|
|
|
|
// 私有构造函数,用于初始化GTaskManager对象的各个成员变量,按照默认值进行初始化,如设置同步相关标记为初始状态,初始化各种数据存储的集合等,保证单例模式下实例的初始化状态统一。
|
|
|
private GTaskManager() {
|
|
|
mSyncing = false;
|
|
|
mCancelled = false;
|
|
|
mGTaskListHashMap = new HashMap<String, TaskList>();
|
|
|
mGTaskHashMap = new HashMap<String, Node>();
|
|
|
mMetaHashMap = new HashMap<String, MetaData>();
|
|
|
mMetaList = null;
|
|
|
mLocalDeleteIdMap = new HashSet<Long>();
|
|
|
mGidToNid = new HashMap<String, Long>();
|
|
|
mNidToGid = new HashMap<Long, String>();
|
|
|
}
|
|
|
|
|
|
// 静态方法,采用双重检查锁定(Double-Checked Locking)的方式实现单例模式,确保多线程环境下能正确获取唯一的GTaskManager实例。
|
|
|
public static synchronized GTaskManager getInstance() {
|
|
|
if (mInstance == null) {
|
|
|
mInstance = new GTaskManager();
|
|
|
}
|
|
|
return mInstance;
|
|
|
}
|
|
|
|
|
|
// 设置当前相关的Activity上下文的方法,主要用于后续获取认证令牌等操作,外部在合适时机调用该方法传入对应的Activity对象来提供必要的上下文环境。
|
|
|
public synchronized void setActivityContext(Activity activity) {
|
|
|
// used for getting authtoken
|
|
|
mActivity = activity;
|
|
|
}
|
|
|
|
|
|
// 执行与Google Tasks服务的同步操作的核心方法,首先判断是否已有同步操作在进行,如果正在同步则直接返回相应状态码,
|
|
|
// 然后初始化相关的成员变量、获取内容解析器对象、设置同步状态为正在进行以及取消状态为未取消等,接着通过GTaskClient进行登录、初始化任务列表、同步内容等一系列操作,
|
|
|
// 根据操作过程中出现的不同异常情况返回相应的状态码,最后在结束时清理相关的数据集合并重置同步状态,若同步被取消则返回取消状态码,否则返回成功状态码。
|
|
|
public int sync(Context context, GTaskASyncTask asyncTask) {
|
|
|
if (mSyncing) {
|
|
|
Log.d(TAG, "Sync is in progress");
|
|
|
return STATE_SYNC_IN_PROGRESS;
|
|
|
}
|
|
|
mContext = context;
|
|
|
mContentResolver = mContext.getContentResolver();
|
|
|
mSyncing = true;
|
|
|
mCancelled = false;
|
|
|
mGTaskListHashMap.clear();
|
|
|
mGTaskHashMap.clear();
|
|
|
mMetaHashMap.clear();
|
|
|
mLocalDeleteIdMap.clear();
|
|
|
mGidToNid.clear();
|
|
|
mNidToGid.clear();
|
|
|
|
|
|
try {
|
|
|
GTaskClient client = GTaskClient.getInstance();
|
|
|
client.resetUpdateArray();
|
|
|
|
|
|
// login google task
|
|
|
if (!mCancelled) {
|
|
|
if (!client.login(mActivity)) {
|
|
|
throw new NetworkFailureException("login google task failed");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// get the task list from google
|
|
|
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
|
|
|
initGTaskList();
|
|
|
|
|
|
// do content sync work
|
|
|
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
|
|
|
syncContent();
|
|
|
} catch (NetworkFailureException e) {
|
|
|
Log.e(TAG, e.toString());
|
|
|
return STATE_NETWORK_ERROR;
|
|
|
} catch (ActionFailureException e) {
|
|
|
Log.e(TAG, e.toString());
|
|
|
return STATE_INTERNAL_ERROR;
|
|
|
} catch (Exception e) {
|
|
|
Log.e(TAG, e.toString();
|
|
|
e.printStackTrace();
|
|
|
return STATE_INTERNAL_ERROR;
|
|
|
} finally {
|
|
|
mGTaskListHashMap.clear();
|
|
|
mGTaskHashMap.clear();
|
|
|
mMetaHashMap.clear();
|
|
|
mLocalDeleteIdMap.clear();
|
|
|
mGidToNid.clear();
|
|
|
mNidToGid.clear();
|
|
|
mSyncing = false;
|
|
|
}
|
|
|
|
|
|
return mCancelled? STATE_SYNC_CANCELLED : STATE_SUCCESS;
|
|
|
}
|
|
|
|
|
|
// 从Google Tasks服务初始化任务列表相关数据的方法,首先判断同步是否已取消,如果取消则直接返回,
|
|
|
// 然后通过GTaskClient获取任务列表信息,先处理元数据相关的任务列表(如查找、创建元数据列表等),再处理普通的任务列表(加载任务列表及其包含的任务等),
|
|
|
// 如果在JSON解析等过程中出现异常则抛出相应的异常表示初始化任务列表操作失败。
|
|
|
private void initGTaskList() throws NetworkFailureException {
|
|
|
if (mCancelled)
|
|
|
return;
|
|
|
GTaskClient client = GTaskClient.getInstance();
|
|
|
try {
|
|
|
JSONArray jsTaskLists = client.getTaskLists();
|
|
|
|
|
|
// init meta list first
|
|
|
mMetaList = null;
|
|
|
for (int i = 0; i < jsTaskLists.length(); i++) {
|
|
|
JSONObject object = jsTaskLists.getJSONObject(i);
|
|
|
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
|
|
|
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
|
|
|
|
|
|
if (name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
|
|
|
mMetaList = new TaskList();
|
|
|
mMetaList.setContentByRemoteJSON(object);
|
|
|
|
|
|
// load meta data
|
|
|
JSONArray jsMetas = client.getTaskList(gid);
|
|
|
for (int j = 0; j < jsMetas.length(); j++) {
|
|
|
object = (JSONObject) jsMetas.getJSONObject(j);
|
|
|
MetaData metaData = new MetaData();
|
|
|
metaData.setContentByRemoteJSON(object);
|
|
|
if (metaData.isWorthSaving()) {
|
|
|
mMetaList.addChildTask(metaData);
|
|
|
if (metaData.getGid()!= null) {
|
|
|
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// create meta list if not existed
|
|
|
if (mMetaList == null) {
|
|
|
mMetaList = new TaskList();
|
|
|
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META);
|
|
|
GTaskClient.getInstance().createTaskList(mMetaList);
|
|
|
}
|
|
|
|
|
|
// init task list
|
|
|
for (int i = 0; i < jsTaskLists.length(); i++) {
|
|
|
JSONObject object = jsTaskLists.getJSONObject(i);
|
|
|
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
|
|
|
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
|
|
|
|
|
|
if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)
|
|
|
&&!name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
|
|
|
TaskList tasklist = new TaskList();
|
|
|
tasklist.setContentByRemoteJSON(object);
|
|
|
mGTaskListHashMap.put(gid, tasklist);
|
|
|
mGTaskHashMap.put(gid, tasklist);
|
|
|
|
|
|
// load tasks
|
|
|
JSONArray jsTasks = client.getTaskList(gid);
|
|
|
for (int j = 0; j < jsTasks.length(); j++) {
|
|
|
object = (JSONObject) jsTasks.getJSONObject(j);
|
|
|
gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
|
|
|
Task task = new Task();
|
|
|
task.setContentByRemoteJSON(object);
|
|
|
if (task.isWorthSaving()) {
|
|
|
task.setMetaInfo(mMetaHashMap.get(gid));
|
|
|
tasklist.addChildTask(task);
|
|
|
mGTaskHashMap.put(gid, task);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} catch (JSONException e) {
|
|
|
Log.e(TAG, e.toString());
|
|
|
e.printStackTrace();
|
|
|
throw new ActionFailureException("initGTaskList: handing JSONObject failed");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void syncContent() throws NetworkFailureException {
|
|
|
// 用于记录当前节点的同步类型,根据不同情况(如本地新增、远程新增、删除等)来确定具体的值,以决定后续执行何种同步操作逻辑。
|
|
|
int syncType;
|
|
|
// 用于查询本地数据库的游标对象,通过ContentResolver进行数据库查询操作,在不同的查询场景下(如查询已删除笔记、现有笔记等)使用,最后需要正确关闭以释放资源。
|
|
|
Cursor c = null;
|
|
|
// 用于存储节点(如任务、任务列表等)对应的唯一标识符(Gid),方便在不同数据结构(如映射表、查询结果等)中查找和关联相关节点数据。
|
|
|
String gid;
|
|
|
// 代表一个节点对象(Node类型,可表示任务、任务列表等),在同步操作中对具体的节点进行各种处理,如根据同步类型执行相应的添加、删除、更新操作等。
|
|
|
Node node;
|
|
|
|
|
|
// 清除本地已删除笔记的ID集合,在每次进行内容同步开始时,先清空之前记录的已删除笔记相关信息,准备重新统计和处理本次同步中的删除情况。
|
|
|
mLocalDeleteIdMap.clear();
|
|
|
|
|
|
// 如果同步操作已被取消(通过mCancelled标志判断),则直接返回,不再执行后续的同步内容相关逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 处理本地已删除笔记的同步逻辑
|
|
|
try {
|
|
|
// 通过ContentResolver查询本地数据库中处于回收站(Notes.ID_TRASH_FOLER)之外且非系统类型(Notes.TYPE_SYSTEM)的笔记信息,获取用于后续判断和处理的数据游标。
|
|
|
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
|
|
|
"(type<>? AND parent_id=?)", new String[] {
|
|
|
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
|
|
|
}, null);
|
|
|
if (c!= null) {
|
|
|
// 遍历查询结果游标,对每一条记录进行处理
|
|
|
while (c.moveToNext()) {
|
|
|
// 获取当前笔记对应的远程节点的唯一标识符(Gid)
|
|
|
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
|
|
|
// 根据Gid从存储远程节点的映射表(mGTaskHashMap)中查找对应的节点对象
|
|
|
node = mGTaskHashMap.get(gid);
|
|
|
if (node!= null) {
|
|
|
// 如果找到了对应的远程节点,说明该笔记在远程端也存在,先从映射表中移除该节点(可能后续会根据同步情况重新添加或更新)
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
// 执行针对远程删除的同步操作,调用doContentSync方法并传入相应的同步类型(Node.SYNC_ACTION_DEL_REMOTE)以及当前节点和游标对象等参数。
|
|
|
doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c);
|
|
|
}
|
|
|
// 将当前本地已删除笔记的ID添加到本地删除笔记ID集合中,用于后续统一清理本地相关记录等操作。
|
|
|
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
|
|
|
}
|
|
|
} else {
|
|
|
// 如果查询游标为null,说明查询本地回收站笔记信息失败,记录相应的警告日志。
|
|
|
Log.w(TAG, "failed to query trash folder");
|
|
|
}
|
|
|
} finally {
|
|
|
// 无论查询操作是否成功,都要确保游标对象被正确关闭,释放相关资源,避免内存泄漏等问题。
|
|
|
if (c!= null) {
|
|
|
c.close();
|
|
|
c = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 先执行文件夹的同步操作,调用syncFolder方法来处理文件夹相关的同步逻辑,包括本地文件夹与远程文件夹的对比、添加、更新等情况。
|
|
|
syncFolder();
|
|
|
|
|
|
// 处理本地数据库中现有笔记(非回收站中的笔记且类型为普通笔记Notes.TYPE_NOTE)的同步逻辑
|
|
|
try {
|
|
|
// 通过ContentResolver查询符合条件的笔记信息,获取游标对象用于后续遍历处理。
|
|
|
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
|
|
|
"(type=? AND parent_id<>?)", new String[] {
|
|
|
String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)
|
|
|
}, NoteColumns.TYPE + " DESC");
|
|
|
if (c!= null) {
|
|
|
// 遍历查询到的每一条笔记记录
|
|
|
while (c.moveToNext()) {
|
|
|
// 获取笔记对应的远程节点的唯一标识符(Gid)
|
|
|
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
|
|
|
// 根据Gid查找对应的远程节点对象
|
|
|
node = mGTaskHashMap.get(gid);
|
|
|
if (node!= null) {
|
|
|
// 如果找到了对应的远程节点,从映射表中移除该节点(后续可能重新添加或更新)
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
// 将远程节点的Gid与本地笔记的ID建立映射关系,方便后续操作中相互查找和关联。
|
|
|
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
|
|
|
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
|
|
|
// 获取当前节点对应的同步类型,通过调用节点的getSyncAction方法并传入游标对象来确定是新增、更新等哪种情况。
|
|
|
syncType = node.getSyncAction(c);
|
|
|
} else {
|
|
|
// 如果未找到对应的远程节点,根据本地笔记的Gid情况判断同步类型,如果Gid为空字符串,说明是本地新增的笔记,同步类型设为Node.SYNC_ACTION_ADD_REMOTE;如果Gid不为空,则认为是远程已删除但本地还存在的情况,同步类型设为Node.SYNC_ACTION_DEL_LOCAL。
|
|
|
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
|
|
|
syncType = Node.SYNC_ACTION_ADD_REMOTE;
|
|
|
} else {
|
|
|
syncType = Node.SYNC_ACTION_DEL_LOCAL;
|
|
|
}
|
|
|
}
|
|
|
// 根据确定的同步类型执行相应的内容同步操作,调用doContentSync方法传入同步类型、节点对象和游标对象等参数。
|
|
|
doContentSync(syncType, node, c);
|
|
|
}
|
|
|
} else {
|
|
|
// 如果查询游标为null,记录查询现有笔记失败的警告日志。
|
|
|
Log.w(TAG, "failed to query existing note in database");
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
// 确保游标对象被正确关闭,释放资源。
|
|
|
if (c!= null) {
|
|
|
c.close();
|
|
|
c = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理剩余的远程节点(即经过前面的处理后,仍留在mGTaskHashMap中的节点,可能是远程新增但本地还未处理的情况)
|
|
|
Iterator<Map.Entry<String, Node>> iter = mGTaskHashMap.entrySet().iterator();
|
|
|
while (iter.hasNext()) {
|
|
|
Map.Entry<String, Node> entry = iter.next();
|
|
|
node = entry.getValue();
|
|
|
// 执行针对本地新增(远程有但本地还没添加的情况)的同步操作,调用doContentSync方法并传入相应同步类型(Node.SYNC_ACTION_ADD_LOCAL)和节点对象等参数。
|
|
|
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
|
|
|
}
|
|
|
|
|
|
// 由于mCancelled可能被其他线程设置(例如外部调用了取消同步的方法),所以需要逐个检查确认是否已取消同步,这里检查如果未取消同步
|
|
|
if (!mCancelled) {
|
|
|
// 尝试批量删除本地已记录的删除笔记,如果批量删除操作失败,抛出相应的异常表示无法批量删除本地已删除的笔记。
|
|
|
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
|
|
|
throw new ActionFailureException("failed to batch-delete local deleted notes");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果同步未被取消
|
|
|
if (!mCancelled) {
|
|
|
// 提交之前暂存的更新操作(通过GTaskClient的commitUpdate方法),确保之前积累的对远程数据的更新请求(如添加、修改等操作)发送到服务器端进行处理。
|
|
|
GTaskClient.getInstance().commitUpdate();
|
|
|
// 刷新本地同步ID,调用refreshLocalSyncId方法,使本地数据的同步标识与远程数据的最新状态保持一致,便于后续判断数据是否需要再次同步等情况。
|
|
|
refreshLocalSyncId();
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
private void syncFolder() throws NetworkFailureException {
|
|
|
// 用于查询本地数据库的游标对象,在不同的文件夹查询场景下使用,最后需要正确关闭以释放资源。
|
|
|
Cursor c = null;
|
|
|
// 用于存储文件夹对应的唯一标识符(Gid),方便在数据结构中查找和关联相关文件夹数据。
|
|
|
String gid;
|
|
|
// 代表一个节点对象(Node类型,这里主要用于表示文件夹节点),对文件夹节点进行各种同步相关的处理,如添加、更新等操作。
|
|
|
Node node;
|
|
|
// 用于记录文件夹的同步类型,根据文件夹在本地和远程的存在情况、名称变化等确定具体的同步操作类型(如新增、更新等)。
|
|
|
int syncType;
|
|
|
|
|
|
// 如果同步操作已被取消(通过mCancelled标志判断),则直接返回,不再执行后续的文件夹同步逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 处理根文件夹的同步逻辑
|
|
|
try {
|
|
|
// 通过ContentResolver查询本地数据库中根文件夹(Notes.ID_ROOT_FOLDER)的信息,获取游标对象用于后续操作。
|
|
|
c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
|
|
|
Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null);
|
|
|
if (c!= null) {
|
|
|
// 将游标移动到第一条记录(通常根文件夹只有一条记录)
|
|
|
c.moveToNext();
|
|
|
// 获取根文件夹对应的远程节点的唯一标识符(Gid)
|
|
|
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
|
|
|
// 根据Gid从存储远程节点的映射表(mGTaskHashMap)中查找对应的节点对象(代表根文件夹的远程节点)
|
|
|
node = mGTaskHashMap.get(gid);
|
|
|
if (node!= null) {
|
|
|
// 如果找到了对应的远程根文件夹节点,从映射表中移除该节点(后续可能根据情况重新添加或更新)
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
// 建立根文件夹的远程Gid与本地ID(Notes.ID_ROOT_FOLDER)的映射关系,方便后续操作中相互查找和关联。
|
|
|
mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
|
|
|
mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid);
|
|
|
// 对于系统文件夹(这里的根文件夹属于系统文件夹),仅在远程名称与本地预期名称不一致时,执行远程更新操作,调用doContentSync方法并传入相应的同步类型(Node.SYNC_ACTION_UPDATE_REMOTE)以及当前节点和游标对象等参数。
|
|
|
if (!node.getName().equals(
|
|
|
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT))
|
|
|
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
|
|
|
} else {
|
|
|
// 如果未找到对应的远程根文件夹节点,执行远程添加操作,调用doContentSync方法并传入相应的同步类型(Node.SYNC_ACTION_ADD_REMOTE)以及当前节点(这里为null,因为远程不存在该节点)和游标对象等参数。
|
|
|
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
|
|
|
}
|
|
|
} else {
|
|
|
// 如果查询游标为null,说明查询根文件夹信息失败,记录相应的警告日志。
|
|
|
Log.w(TAG, "failed to query root folder");
|
|
|
}
|
|
|
} finally {
|
|
|
// 无论查询操作是否成功,都要确保游标对象被正确关闭,释放相关资源,避免内存泄漏等问题。
|
|
|
if (c!= null) {
|
|
|
c.close();
|
|
|
c = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理通话记录文件夹(Notes.ID_CALL_RECORD_FOLDER)的同步逻辑,与根文件夹的处理逻辑类似,只是针对的是通话记录文件夹相关情况进行判断和操作。
|
|
|
try {
|
|
|
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)",
|
|
|
new String[] {
|
|
|
String.valueOf(Notes.ID_CALL_RECORD_FOLDER)
|
|
|
}, null);
|
|
|
if (c!= null) {
|
|
|
if (c.moveToNext()) {
|
|
|
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
|
|
|
node = mGTaskHashMap.get(gid);
|
|
|
if (node!= null) {
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
mGIdToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER);
|
|
|
mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid);
|
|
|
if (!node.getName().equals(
|
|
|
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE))
|
|
|
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
|
|
|
} else {
|
|
|
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
Log.w(TAG, "failed to query call note folder");
|
|
|
}
|
|
|
} finally {
|
|
|
if (c!= null) {
|
|
|
c.close();
|
|
|
c = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理本地现有文件夹(非回收站中的文件夹且类型为文件夹Notes.TYPE_FOLDER)的同步逻辑,通过查询获取游标对象并遍历处理每一个文件夹记录。
|
|
|
try {
|
|
|
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
|
|
|
"(type=? AND parent_id<>?)", new String[] {
|
|
|
String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)
|
|
|
}, NoteColumns.TYPE + " DESC");
|
|
|
if (c!= null) {
|
|
|
while (c.moveToNext()) {
|
|
|
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
|
|
|
node = mGTaskHashMap.get(gid);
|
|
|
if (node!= null) {
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
|
|
|
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
|
|
|
syncType = node.getSyncAction(c);
|
|
|
} else {
|
|
|
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
|
|
|
syncType = Node.SYNC_ACTION_ADD_REMOTE;
|
|
|
} else {
|
|
|
syncType = Node.SYNC_ACTION_DEL_LOCAL;
|
|
|
}
|
|
|
}
|
|
|
doContentSync(syncType, node, c);
|
|
|
}
|
|
|
} else {
|
|
|
Log.w(TAG, "failed to query existing folder");
|
|
|
}
|
|
|
} finally {
|
|
|
if (c!= null) {
|
|
|
c.close();
|
|
|
c = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理远程新增的文件夹(即本地不存在但远程有,通过对比本地存储的任务列表映射表mGTaskListHashMap和节点映射表mGTaskHashMap来确定)
|
|
|
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
|
|
|
while (iter.hasNext()) {
|
|
|
Map.Entry<String, TaskList> entry = iter.next();
|
|
|
gid = entry.getKey();
|
|
|
node = entry.getValue();
|
|
|
if (mGTaskHashMap.containsKey(gid)) {
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果同步未被取消,提交之前暂存的更新操作(通过GTaskClient的commitUpdate方法),确保与文件夹相关的更新操作能发送到服务器端进行处理。
|
|
|
if (!mCancelled)
|
|
|
GTaskClient.getInstance().commitUpdate();
|
|
|
}
|
|
|
|
|
|
private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException {
|
|
|
// 如果同步操作已被取消(通过检查mCancelled标志),则直接返回,不执行后续的同步操作逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
MetaData meta;
|
|
|
switch (syncType) {
|
|
|
// 同步类型为本地添加(Node.SYNC_ACTION_ADD_LOCAL)的情况
|
|
|
case Node.SYNC_ACTION_ADD_LOCAL:
|
|
|
// 调用addLocalNode方法来处理在本地添加节点的相关逻辑,比如创建本地记录、更新相关映射关系等操作。
|
|
|
addLocalNode(node);
|
|
|
break;
|
|
|
// 同步类型为远程添加(Node.SYNC_ACTION_ADD_REMOTE)的情况
|
|
|
case Node.SYNC_ACTION_ADD_REMOTE:
|
|
|
// 调用addRemoteNode方法来处理向远程(如Google Tasks服务)添加节点的相关逻辑,同时传入当前节点和游标对象,用于获取相关数据辅助操作,比如创建任务、任务列表等远程操作以及相应的本地记录更新。
|
|
|
addRemoteNode(node, c);
|
|
|
break;
|
|
|
// 同步类型为本地删除(Node.SYNC_ACTION_DEL_LOCAL)的情况
|
|
|
case Node.SYNC_ACTION_DEL_LOCAL:
|
|
|
// 从元数据映射表(mMetaHashMap)中根据当前节点对应的Gid(从游标获取的对应列值)获取对应的元数据对象。
|
|
|
meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
|
|
|
if (meta!= null) {
|
|
|
// 如果获取到了对应的元数据对象,调用GTaskClient的deleteNode方法删除该元数据节点,以保持远程数据与本地删除操作的一致性。
|
|
|
GTaskClient.getInstance().deleteNode(meta);
|
|
|
}
|
|
|
// 将当前本地要删除的节点的ID添加到本地删除笔记ID集合(mLocalDeleteIdMap)中,用于后续统一清理本地相关记录等操作。
|
|
|
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
|
|
|
break;
|
|
|
// 同步类型为远程删除(Node.SYNC_ACTION_DEL_REMOTE)的情况
|
|
|
case Node.SYNC_ACTION_DEL_REMOTE:
|
|
|
// 从元数据映射表(mMetaHashMap)中根据当前节点的Gid获取对应的元数据对象。
|
|
|
meta = mMetaHashMap.get(node.getGid());
|
|
|
if (meta!= null) {
|
|
|
// 如果对应的元数据对象存在,调用GTaskClient的deleteNode方法删除该元数据节点。
|
|
|
GTaskClient.getInstance().deleteNode(meta);
|
|
|
}
|
|
|
// 调用GTaskClient的deleteNode方法删除当前要处理的远程节点本身,实现远程数据的删除操作。
|
|
|
GTaskClient.getInstance().deleteNode(node);
|
|
|
break;
|
|
|
// 同步类型为本地更新(Node.SYNC_ACTION_UPDATE_LOCAL)的情况
|
|
|
case Node.SYNC_ACTION_UPDATE_LOCAL:
|
|
|
// 调用updateLocalNode方法来处理在本地更新节点相关信息的逻辑,比如更新本地数据库中的记录内容、相关标识等操作。
|
|
|
updateLocalNode(node, c);
|
|
|
break;
|
|
|
// 同步类型为远程更新(Node.SYNC_ACTION_UPDATE_REMOTE)的情况
|
|
|
case Node.SYNC_ACTION_UPDATE_REMOTE:
|
|
|
// 调用updateRemoteNode方法来处理向远程(如Google Tasks服务)更新节点相关信息的逻辑,比如更新远程任务、任务列表的内容等操作,同时传入当前节点和游标对象用于获取相关数据辅助更新。
|
|
|
updateRemoteNode(node, c);
|
|
|
break;
|
|
|
// 同步类型为更新冲突(Node.SYNC_ACTION_UPDATE_CONFLICT)的情况
|
|
|
case Node.SYNC_ACTION_UPDATE_CONFLICT:
|
|
|
// 这里注释提到合并双方修改可能是个好主意,但目前只是简单采用本地更新的方式来处理。
|
|
|
// 调用updateRemoteNode方法,按照本地更新的方式来处理这种冲突情况(向远程更新节点信息),传入当前节点和游标对象辅助操作。
|
|
|
updateRemoteNode(node, c);
|
|
|
break;
|
|
|
// 同步类型为无操作(Node.SYNC_ACTION_NONE)的情况,直接跳过不执行任何操作。
|
|
|
case Node.SYNC_ACTION_NONE:
|
|
|
break;
|
|
|
// 同步类型为错误(Node.SYNC_ACTION_ERROR)或其他未定义的情况,抛出异常表示遇到了未知的同步操作类型。
|
|
|
case Node.SYNC_ACTION_ERROR:
|
|
|
default:
|
|
|
throw new ActionFailureException("unkown sync action type");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void addLocalNode(Node node) throws NetworkFailureException {
|
|
|
// 如果同步操作已被取消(通过检查mCancelled标志),则直接返回,不执行后续添加本地节点的逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
SqlNote sqlNote;
|
|
|
// 判断当前节点是否是任务列表(TaskList类型)的实例
|
|
|
if (node instanceof TaskList) {
|
|
|
// 如果是根文件夹(通过名称判断是否匹配特定的根文件夹标识)
|
|
|
if (node.getName().equals(
|
|
|
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) {
|
|
|
// 创建一个SqlNote对象,关联到本地的根文件夹(传入Notes.ID_ROOT_FOLDER作为参数),用于后续操作本地数据库相关记录。
|
|
|
sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER);
|
|
|
} else if (node.getName().equals(
|
|
|
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) {
|
|
|
// 如果是通话记录文件夹(通过名称判断),创建一个SqlNote对象,关联到本地的通话记录文件夹(传入Notes.ID_CALL_RECORD_FOLDER作为参数)。
|
|
|
sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER);
|
|
|
} else {
|
|
|
// 如果是其他普通的任务列表,创建一个新的SqlNote对象,用于后续设置相关内容并插入到本地数据库。
|
|
|
sqlNote = new SqlNote(mContext);
|
|
|
// 设置SqlNote对象的内容为从当前节点获取的本地JSON格式内容(通过节点的getLocalJSONFromContent方法获取),该内容包含了任务列表的相关详细信息。
|
|
|
sqlNote.setContent(node.getLocalJSONFromContent());
|
|
|
// 设置父文件夹ID为根文件夹ID(Notes.ID_ROOT_FOLDER),表示该任务列表在本地的层级关系。
|
|
|
sqlNote.setParentId(Notes.ID_ROOT_FOLDER);
|
|
|
}
|
|
|
} else {
|
|
|
// 如果当前节点不是任务列表,而是普通任务(Task类型)等其他节点情况,创建一个新的SqlNote对象用于后续操作。
|
|
|
sqlNote = new SqlNote(mContext);
|
|
|
JSONObject js = node.getLocalJSONFromContent();
|
|
|
try {
|
|
|
// 如果从节点获取的本地JSON内容中包含特定的元数据头部笔记标识(GTaskStringUtils.META_HEAD_NOTE)
|
|
|
if (js.has(GTaskStringUtils.META_HEAD_NOTE)) {
|
|
|
// 获取对应的笔记JSON对象。
|
|
|
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
|
|
|
if (note.has(NoteColumns.ID)) {
|
|
|
long id = note.getLong(NoteColumns.ID);
|
|
|
// 检查该笔记ID在本地笔记数据库中是否已存在(通过DataUtils的existInNoteDatabase方法),如果已存在,表示该ID不可用,需要移除这个ID字段(可能重新生成新的ID等情况)。
|
|
|
if (DataUtils.existInNoteDatabase(mContentResolver, id)) {
|
|
|
note.remove(NoteColumns.ID);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果从节点获取的本地JSON内容中包含特定的元数据头部数据标识(GTaskStringUtils.META_HEAD_DATA)
|
|
|
if (js.has(GTaskStringUtils.META_HEAD_DATA)) {
|
|
|
// 获取对应的JSON数组,里面包含了多个数据对象。
|
|
|
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
|
|
|
for (int i = 0; i < dataArray.length(); i++) {
|
|
|
JSONObject data = dataArray.getJSONObject(i);
|
|
|
if (data.has(DataColumns.ID)) {
|
|
|
long dataId = data.getLong(DataColumns.ID);
|
|
|
// 检查该数据ID在本地数据数据库中是否已存在(通过DataUtils的existInDataDatabase方法),如果已存在,移除这个ID字段(可能重新生成等情况)。
|
|
|
if (DataUtils.existInDataDatabase(mContentResolver, dataId)) {
|
|
|
data.remove(DataColumns.ID);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} catch (JSONException e) {
|
|
|
// 如果在JSON解析等操作过程中出现异常,记录相应的警告日志,并打印异常堆栈信息。
|
|
|
Log.w(TAG, e.toString());
|
|
|
e.printStackTrace();
|
|
|
}
|
|
|
// 设置SqlNote对象的内容为处理后的JSON对象,包含了任务相关的正确数据信息。
|
|
|
sqlNote.setContent(js);
|
|
|
|
|
|
// 获取当前任务节点的父节点的Gid对应的本地节点ID,通过查找mGidToNid映射表来获取,用于确定该任务在本地的正确父级关系。
|
|
|
Long parentId = mGidToNid.get(((Task) node).getParent().getGid());
|
|
|
if (parentId == null) {
|
|
|
// 如果找不到对应的父节点ID,记录错误日志,并抛出异常表示无法添加本地节点,因为缺少父节点关联信息。
|
|
|
Log.e(TAG, "cannot find task's parent id locally");
|
|
|
throw new ActionFailureException("cannot add local node");
|
|
|
}
|
|
|
// 设置SqlNote对象的父节点ID为获取到的父节点ID对应的长整型值,建立本地任务节点与父节点的关联关系。
|
|
|
sqlNote.setParentId(parentId.longValue());
|
|
|
}
|
|
|
|
|
|
// 设置SqlNote对象对应的远程节点的Gid(即当前要添加的本地节点对应的远程唯一标识符),用于后续关联本地与远程数据。
|
|
|
sqlNote.setGtaskId(node.getGid());
|
|
|
// 将SqlNote对象插入到本地数据库中,commit方法传入false可能表示以特定的插入模式(比如非自动更新相关关联数据等情况,具体看SqlNote类的实现)进行插入操作。
|
|
|
sqlNote.commit(false);
|
|
|
|
|
|
// 在远程Gid与本地ID的映射表(mGidToNid)中添加一条记录,将当前节点的Gid与刚插入本地数据库的SqlNote对象的ID建立映射关系,方便后续查找和同步操作使用。
|
|
|
mGidToNid.put(node.getGid(), sqlNote.getId());
|
|
|
// 在本地ID与远程Gid的映射表(mNidToGid)中添加反向的映射关系,即本地ID对应远程Gid,保持双向关联。
|
|
|
mNidToGid.put(sqlNote.getId(), node.getGid());
|
|
|
|
|
|
// 调用updateRemoteMeta方法,根据当前节点的Gid和SqlNote对象来更新远程元数据相关信息,保持数据的一致性和完整性。
|
|
|
updateRemoteMeta(node.getGid(), sqlNote);
|
|
|
}
|
|
|
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
|
|
|
// 如果同步操作已被取消(通过检查mCancelled标志),则直接返回,不执行后续更新本地节点的逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
SqlNote sqlNote;
|
|
|
// 更新本地的笔记信息,创建一个SqlNote对象,通过传入上下文和游标对象来初始化,该对象用于操作本地数据库中的对应记录。
|
|
|
sqlNote = new SqlNote(mContext, c);
|
|
|
// 设置SqlNote对象的内容为从当前节点获取的本地JSON格式内容(通过节点的getLocalJSONFromContent方法获取),用于更新本地数据库中记录的具体内容。
|
|
|
sqlNote.setContent(node.getLocalJSONFromContent());
|
|
|
|
|
|
Long parentId = (node instanceof Task)? mGidToNid.get(((Task) node).getParent().getGid())
|
|
|
: new Long(Notes.ID_ROOT_FOLDER);
|
|
|
if (parentId == null) {
|
|
|
// 如果获取不到对应的父节点ID(对于任务节点通过查找映射表获取父节点Gid对应的本地ID,对于非任务节点默认设为根文件夹ID,如果还是获取不到则为null),记录错误日志,并抛出异常表示无法更新本地节点,因为缺少父节点关联信息。
|
|
|
Log.e(TAG, "cannot find task's parent id locally");
|
|
|
throw new ActionFailureException("cannot update local node");
|
|
|
}
|
|
|
// 设置SqlNote对象的父节点ID为获取到的有效父节点ID对应的长整型值,建立本地节点与正确父节点的关联关系,确保更新操作在正确的层级下进行。
|
|
|
sqlNote.setParentId(parentId.longValue());
|
|
|
// 将更新后的SqlNote对象提交到本地数据库,commit方法传入true可能表示以另一种更新模式(比如自动更新相关关联数据等情况,具体看SqlNote类的实现)进行更新操作,使本地数据库中的记录与当前节点的最新信息保持一致。
|
|
|
sqlNote.commit(true);
|
|
|
|
|
|
// 调用updateRemoteMeta方法,根据当前节点的Gid和更新后的SqlNote对象来更新远程元数据相关信息,保持本地与远程数据的同步和一致性。
|
|
|
updateRemoteMeta(node.getGid(), sqlNote);
|
|
|
}
|
|
|
|
|
|
private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
|
|
|
// 首先判断同步操作是否已被取消,如果已取消则直接返回,不执行后续添加远程节点的逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 创建一个SqlNote对象,通过传入当前上下文(mContext)和游标(c)来初始化,该对象用于后续对本地数据库中相关记录进行操作,比如获取记录信息、更新记录状态等。
|
|
|
SqlNote sqlNote = new SqlNote(mContext, c);
|
|
|
Node n;
|
|
|
|
|
|
// 判断当前SqlNote所代表的是否是笔记类型(通过isNoteType方法判断),如果是笔记类型,则执行向远程添加任务节点的相关逻辑。
|
|
|
if (sqlNote.isNoteType()) {
|
|
|
// 创建一个新的Task任务对象,用于表示要添加到远程的任务。
|
|
|
Task task = new Task();
|
|
|
// 设置任务对象的内容,通过调用setContentByLocalJSON方法并传入从SqlNote对象获取的本地JSON格式内容,将本地存储的任务相关信息传递给该任务对象,以便后续发送到远程进行创建操作。
|
|
|
task.setContentByLocalJSON(sqlNote.getContent());
|
|
|
|
|
|
// 尝试获取该任务的父任务列表对应的远程唯一标识符(Gid),通过查找本地ID与远程Gid的映射表(mNidToGid),以SqlNote对象中记录的父节点ID为键来获取对应的值(即父任务列表的Gid)。
|
|
|
String parentGid = mNidToGid.get(sqlNote.getParentId());
|
|
|
if (parentGid == null) {
|
|
|
// 如果无法获取到父任务列表的Gid,说明缺少必要的关联信息,记录错误日志,并抛出异常表示无法添加远程任务,因为找不到对应的父任务列表。
|
|
|
Log.e(TAG, "cannot find task's parent tasklist");
|
|
|
throw new ActionFailureException("cannot add remote task");
|
|
|
}
|
|
|
// 根据获取到的父任务列表的Gid,从存储远程任务列表的映射表(mGTaskListHashMap)中获取对应的任务列表对象,并调用其addChildTask方法将新创建的任务添加到该父任务列表中,建立任务的层级关系。
|
|
|
mGTaskListHashMap.get(parentGid).addChildTask(task);
|
|
|
|
|
|
// 调用GTaskClient的createTask方法,将创建好的任务对象发送到远程(例如Google Tasks服务)进行创建操作,实现向远程添加任务节点的功能。
|
|
|
GTaskClient.getInstance().createTask(task);
|
|
|
// 将创建好的远程任务节点(以Node类型表示,这里通过强制类型转换将Task类型转换为Node类型)赋值给n,方便后续统一处理新添加的远程节点相关操作。
|
|
|
n = (Node) task;
|
|
|
|
|
|
// 调用updateRemoteMeta方法,传入刚创建的任务的Gid以及对应的SqlNote对象,用于更新与该任务相关的远程元数据信息,确保远程数据的完整性和一致性,比如记录任务相关的额外描述、属性等元数据信息。
|
|
|
updateRemoteMeta(task.getGid(), sqlNote);
|
|
|
} else {
|
|
|
// 如果不是笔记类型,说明要处理的是任务列表(文件夹)相关的添加操作,先初始化任务列表对象为null,后续根据情况查找或创建相应的任务列表。
|
|
|
TaskList tasklist = null;
|
|
|
|
|
|
// 构建文件夹名称,先添加特定的前缀(GTaskStringUtils.MIUI_FOLDER_PREFFIX),然后根据SqlNote对象的ID情况添加不同的后缀来确定具体的文件夹名称。
|
|
|
String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;
|
|
|
if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) {
|
|
|
folderName += GTaskStringUtils.FOLDER_DEFAULT;
|
|
|
} else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) {
|
|
|
folderName += GTaskStringUtils.FOLDER_CALL_NOTE;
|
|
|
} else {
|
|
|
folderName += sqlNote.getSnippet();
|
|
|
}
|
|
|
|
|
|
// 通过迭代器遍历存储远程任务列表的映射表(mGTaskListHashMap)中的所有元素(以键值对形式,键为任务列表的Gid,值为任务列表对象),用于查找是否已存在同名的任务列表。
|
|
|
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
|
|
|
while (iter.hasNext()) {
|
|
|
Map.Entry<String, TaskList> entry = iter.next();
|
|
|
String gid = entry.getKey();
|
|
|
TaskList list = entry.getValue();
|
|
|
|
|
|
// 如果找到名称与构建的folderName相同的任务列表,说明该文件夹在远程已存在,将其赋值给tasklist,并判断是否在另一个存储远程节点的映射表(mGTaskHashMap)中存在该任务列表对应的节点,如果存在则移除(可能后续会重新添加或更新相关信息),然后跳出循环。
|
|
|
if (list.getName().equals(folderName)) {
|
|
|
tasklist = list;
|
|
|
if (mGTaskHashMap.containsKey(gid)) {
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
}
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果经过上述查找后tasklist仍为null,说明没有找到同名的任务列表,即该文件夹在远程不存在,需要创建新的任务列表。
|
|
|
if (tasklist == null) {
|
|
|
// 创建一个新的TaskList任务列表对象,用于表示要添加到远程的任务列表(文件夹)。
|
|
|
tasklist = new TaskList();
|
|
|
// 设置任务列表对象的内容,通过调用setContentByLocalJSON方法并传入从SqlNote对象获取的本地JSON格式内容,将本地存储的任务列表相关信息传递给该对象,以便后续发送到远程进行创建操作。
|
|
|
tasklist.setContentByLocalJSON(sqlNote.getContent());
|
|
|
// 调用GTaskClient的createTaskList方法,将创建好的任务列表对象发送到远程(例如Google Tasks服务)进行创建操作,实现向远程添加任务列表(文件夹)的功能。
|
|
|
GTaskClient.getInstance().createTaskList(tasklist);
|
|
|
// 将新创建的任务列表添加到存储远程任务列表的映射表(mGTaskListHashMap)中,以其Gid为键,任务列表对象本身为值,方便后续查找和管理远程任务列表数据。
|
|
|
mGTaskListHashMap.put(tasklist.getGid(), tasklist);
|
|
|
}
|
|
|
// 将创建好的远程任务列表节点(以Node类型表示,这里通过强制类型转换将TaskList类型转换为Node类型)赋值给n,方便后续统一处理新添加的远程节点相关操作。
|
|
|
n = (Node) tasklist;
|
|
|
}
|
|
|
|
|
|
// 更新本地SqlNote对象对应的远程节点的Gid,将其设置为新添加或找到的远程节点(无论是任务还是任务列表转换后的Node类型节点)的Gid,以此建立本地记录与远程节点的关联关系,便于后续同步操作中进行对应查找等操作。
|
|
|
sqlNote.setGtaskId(n.getGid());
|
|
|
// 将SqlNote对象提交到本地数据库,传入false可能表示以特定的提交模式(例如不自动触发某些关联数据的更新等情况,具体取决于SqlNote类的实现逻辑)进行提交操作,可能执行插入或更新本地数据库记录的相关操作,使本地数据库中的记录与远程操作结果保持一定的一致性。
|
|
|
sqlNote.commit(false);
|
|
|
// 重置SqlNote对象的本地修改标志(例如可能用于标记该记录在本地是否有过修改等情况,具体功能看其在类中的定义),将其恢复到初始或未修改的状态,可能是为了后续准确判断记录的修改情况等操作。
|
|
|
sqlNote.resetLocalModified();
|
|
|
// 再次将SqlNote对象提交到本地数据库,传入true可能表示以另一种提交模式(例如会自动更新相关联的数据等情况,同样取决于SqlNote类的实现逻辑)进行提交操作,确保本地数据库中的记录状态完全更新,与远程操作及相关处理后的结果准确匹配。
|
|
|
sqlNote.commit(true);
|
|
|
|
|
|
// 在远程Gid与本地ID的映射表(mGidToNid)中添加一条映射记录,将新添加的远程节点的Gid作为键,对应的本地SqlNote对象的ID作为值,方便后续在同步过程中通过远程Gid快速查找本地对应的记录ID,用于数据关联和操作。
|
|
|
mGidToNid.put(n.getGid(), sqlNote.getId());
|
|
|
// 在本地ID与远程Gid的映射表(mNidToGid)中添加反向的映射记录,即将本地SqlNote对象的ID作为键,对应的新添加的远程节点的Gid作为值,建立双向的映射关系,便于在不同的操作场景下进行本地与远程数据的相互查找和关联。
|
|
|
mNidToGid.put(sqlNote.getId(), n.getGid());
|
|
|
}
|
|
|
private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
|
|
|
// 首先判断同步操作是否已被取消,如果已取消则直接返回,不执行后续更新远程节点的逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 创建一个SqlNote对象,通过传入当前上下文(mContext)和游标(c)来初始化,该对象用于获取本地数据库中对应记录的相关信息,以辅助更新远程节点的操作。
|
|
|
SqlNote sqlNote = new SqlNote(mContext, c);
|
|
|
|
|
|
// 更新远程节点的内容,通过调用节点(node)的setContentByLocalJSON方法,并传入从SqlNote对象获取的本地JSON格式内容,将本地存储的最新节点信息传递给远程节点,实现远程节点内容的更新,使其与本地记录保持一致。
|
|
|
node.setContentByLocalJSON(sqlNote.getContent());
|
|
|
// 调用GTaskClient的addUpdateNode方法,将更新后的节点对象发送到远程(例如Google Tasks服务),通知远程服务对相应的节点进行更新操作,具体的更新实现取决于远程服务端的逻辑。
|
|
|
GTaskClient.getInstance().addUpdateNode(node);
|
|
|
|
|
|
// 调用updateRemoteMeta方法,传入当前节点的Gid以及对应的SqlNote对象,用于更新与该节点相关的远程元数据信息,确保远程数据的完整性和一致性,因为节点内容更新后可能对应的元数据也需要相应调整。
|
|
|
updateRemoteMeta(node.getGid(), sqlNote);
|
|
|
|
|
|
// 判断当前SqlNote对象是否代表笔记类型(即是否是任务节点),如果是,则执行与任务移动相关的逻辑(可能涉及到任务在不同任务列表之间切换等情况)。
|
|
|
if (sqlNote.isNoteType()) {
|
|
|
// 将当前节点强制转换为Task类型,方便后续操作任务相关的属性,比如获取父任务列表等信息。
|
|
|
Task task = (Task) node;
|
|
|
// 获取任务当前所在的父任务列表对象,用于后续对比和判断是否需要移动任务到其他任务列表。
|
|
|
TaskList preParentList = task.getParent();
|
|
|
|
|
|
// 尝试获取任务的目标父任务列表对应的远程唯一标识符(Gid),通过查找本地ID与远程Gid的映射表(mNidToGid),以SqlNote对象中记录的父节点ID为键来获取对应的值(即目标父任务列表的Gid)。
|
|
|
String curParentGid = mNidToGid.get(sqlNote.getParentId());
|
|
|
if (curParentGid == null) {
|
|
|
// 如果无法获取到目标父任务列表的Gid,说明缺少必要的关联信息,记录错误日志,并抛出异常表示无法更新远程任务,因为找不到对应的目标父任务列表。
|
|
|
Log.e(TAG, "cannot find task's parent tasklist");
|
|
|
throw new ActionFailureException("cannot update remote task");
|
|
|
}
|
|
|
// 根据获取到的目标父任务列表的Gid,从存储远程任务列表的映射表(mGTaskListHashMap)中获取对应的任务列表对象,用于后续操作任务在不同任务列表之间的移动。
|
|
|
TaskList curParentList = mGTaskListHashMap.get(curParentGid);
|
|
|
|
|
|
// 判断当前任务的原父任务列表和目标父任务列表是否不同,如果不同,则说明需要将任务从原父任务列表移动到目标父任务列表。
|
|
|
if (preParentList!= curParentList) {
|
|
|
// 从原父任务列表中移除当前任务,调用原父任务列表的removeChildTask方法,将任务从原层级关系中移除。
|
|
|
preParentList.removeChildTask(task);
|
|
|
// 将当前任务添加到目标父任务列表中,调用目标父任务列表的addChildTask方法,建立任务在新的目标父任务列表中的层级关系,完成任务的移动操作。
|
|
|
curParentList.addChildTask(task);
|
|
|
// 调用GTaskClient的moveTask方法,通知远程服务(例如Google Tasks服务)执行任务的移动操作,将任务从原父任务列表移动到目标父任务列表,确保远程数据的任务层级关系与本地操作结果一致。
|
|
|
GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 清除SqlNote对象的本地修改标志(例如可能用于标记该记录在本地是否有过修改等情况,具体功能看其在类中的定义),将其恢复到初始或未修改的状态,可能是为了后续准确判断记录的修改情况等操作,以及与远程数据保持一致的状态标识。
|
|
|
sqlNote.resetLocalModified();
|
|
|
// 将SqlNote对象再次提交到本地数据库,传入true可能表示以特定的提交模式(例如会自动更新相关联的数据等情况,具体取决于SqlNote类的实现逻辑)进行提交操作,确保本地数据库中的记录状态与远程节点更新后的情况准确匹配,保持数据的一致性。
|
|
|
sqlNote.commit(true);
|
|
|
}
|
|
|
|
|
|
private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
|
|
|
// 首先判断传入的SqlNote对象是否为空,并且是否代表笔记类型(通过isNoteType方法判断),只有满足这两个条件才执行后续更新远程元数据的逻辑,可能是因为只有笔记类型的节点才有对应的元数据需要处理,且需要有效的SqlNote对象来获取相关内容信息。
|
|
|
if (sqlNote!= null && sqlNote.isNoteType()) {
|
|
|
// 尝试从存储元数据的映射表(mMetaHashMap)中,以传入的节点的Gid为键,获取对应的元数据对象(MetaData类型),用于后续判断该元数据是否已存在以及相应的更新操作。
|
|
|
MetaData metaData = mMetaHashMap.get(gid);
|
|
|
if (metaData!= null) {
|
|
|
// 如果获取到了对应的元数据对象,说明该元数据已存在,调用其setMeta方法,传入节点的Gid以及从SqlNote对象获取的内容信息,用于更新该元数据对象的具体内容,使其与本地最新的节点信息保持一致,比如更新任务相关的额外描述、属性等元数据信息。
|
|
|
metaData.setMeta(gid, sqlNote.getContent());
|
|
|
// 调用GTaskClient的addUpdateNode方法,将更新后的元数据对象发送到远程(例如Google Tasks服务),通知远程服务对相应的元数据进行更新操作,确保远程元数据与本地数据的一致性。
|
|
|
GTaskClient.getInstance().addUpdateNode(metaData);
|
|
|
} else {
|
|
|
// 如果没有获取到对应的元数据对象,说明该元数据不存在,需要创建新的元数据对象。
|
|
|
metaData = new MetaData();
|
|
|
// 调用新创建的元数据对象的setMeta方法,传入节点的Gid以及从SqlNote对象获取的内容信息,用于初始化该元数据对象的内容,使其包含与当前节点相关的必要信息。
|
|
|
metaData.setMeta(gid, sqlNote.getContent());
|
|
|
// 将新创建的元数据对象添加到存储元数据的任务列表(mMetaList)中,作为其子任务(可能是一种组织和管理元数据的方式,具体看相关类的实现逻辑),建立元数据与任务列表的关联关系。
|
|
|
mMetaList.addChildTask(metaData);
|
|
|
// 将新创建的元数据对象添加到存储元数据的映射表(mMetaHashMap)中,以节点的Gid为键,元数据对象本身为值,方便后续查找和管理元数据信息,确保元数据与对应节点的关联关系在整个同步过程中可维护。
|
|
|
mMetaHashMap.put(gid, metaData);
|
|
|
// 调用GTaskClient的createTask方法,将新创建的元数据对象发送到远程(例如Google Tasks服务)进行创建操作,实现向远程添加新的元数据信息的功能,确保远程数据中包含最新的元数据内容与本地数据匹配。
|
|
|
GTaskClient.getInstance().createTask(metaData);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void refreshLocalSyncId() throws NetworkFailureException {
|
|
|
// 首先判断同步操作是否已经被取消,如果 `mCancelled` 标志为 `true`,则直接返回,不执行后续刷新本地同步 ID 的相关逻辑。
|
|
|
if (mCancelled) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 获取最新的 gtask 列表相关操作,以下几步是为了清除之前可能缓存的相关数据结构中的信息,准备重新获取最新的数据进行后续处理。
|
|
|
// 清空存储远程节点信息的映射表 `mGTaskHashMap`,清除之前记录的远程节点相关数据,避免旧数据影响后续基于最新数据的操作。
|
|
|
mGTaskHashMap.clear();
|
|
|
// 清空存储远程任务列表信息的映射表 `mGTaskListHashMap`,同样是为了清理旧的任务列表相关缓存数据。
|
|
|
mGTaskListHashMap.clear();
|
|
|
// 清空存储元数据信息的映射表 `mMetaHashMap`,确保后续元数据操作基于最新获取的数据。
|
|
|
mMetaHashMap.clear();
|
|
|
// 调用 `initGTaskList` 方法,该方法的作用应该是从远程数据源(比如可能是和某个远程任务服务交互)获取最新的任务列表相关数据,并对相关数据结构进行初始化填充,为后续刷新本地同步 ID 的操作准备好基础数据。
|
|
|
initGTaskList();
|
|
|
|
|
|
// 定义一个游标对象 `c`,用于后续查询本地数据库操作,初始化为 `null`,在使用前需要正确初始化并在最后确保关闭以释放资源。
|
|
|
Cursor c = null;
|
|
|
try {
|
|
|
// 通过 `ContentResolver` 发起对本地数据库的查询操作,查询的是 `Notes.CONTENT_NOTE_URI` 所指向的内容(通常表示笔记相关的数据表)。
|
|
|
// 查询所需要获取的列信息由 `SqlNote.PROJECTION_NOTE` 定义,具体包含哪些列要看 `SqlNote` 类中该常量的定义情况。
|
|
|
// 查询的条件是笔记类型不等于系统类型(通过 `(type<>?` 判断,具体值取自 `Notes.TYPE_SYSTEM`)并且父节点 ID 不等于回收站文件夹的 ID(通过 `parent_id<>?` 判断,具体值取自 `Notes.ID_TRASH_FOLER`),以筛选出符合要求的笔记记录。
|
|
|
// 最后的排序方式按照 `NoteColumns.TYPE + " DESC"`,也就是按照 `NoteColumns.TYPE` 定义的类型字段进行降序排列,具体排序的逻辑和作用取决于该字段的含义。
|
|
|
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
|
|
|
"(type<>? AND parent_id<>?)", new String[] {
|
|
|
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
|
|
|
}, NoteColumns.TYPE + " DESC");
|
|
|
// 如果查询成功,即游标 `c` 不为 `null`,则开始遍历查询结果集,对每一条符合条件的笔记记录进行处理。
|
|
|
if (c!= null) {
|
|
|
while (c.moveToNext()) {
|
|
|
// 从当前游标所指向的记录中获取对应的远程节点唯一标识符(Gid),通过 `SqlNote.GTASK_ID_COLUMN` 指定的列来获取,该列应该存储了节点在远程端对应的唯一标识信息。
|
|
|
String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
|
|
|
// 根据获取到的远程节点 Gid,尝试从 `mGTaskHashMap` 映射表中获取对应的节点对象,如果能获取到,表示该远程节点在本地有对应的记录关联。
|
|
|
Node node = mGTaskHashMap.get(gid);
|
|
|
if (node!= null) {
|
|
|
// 如果找到了对应的节点对象,先从 `mGTaskHashMap` 映射表中移除该节点,可能是为了避免重复处理或者更新相关状态等操作(具体要看后续逻辑对该映射表的使用情况)。
|
|
|
mGTaskHashMap.remove(gid);
|
|
|
// 创建一个 `ContentValues` 对象,用于存放要更新到数据库中的数据值,这里主要是要更新同步 ID(`NoteColumns.SYNC_ID`)相关的值。
|
|
|
ContentValues values = new ContentValues();
|
|
|
// 将当前节点对象的最后修改时间(通过 `node.getLastModified()` 获取)设置到 `ContentValues` 对象中,对应的键为 `NoteColumns.SYNC_ID`,意味着要将这个最后修改时间更新到本地数据库中对应笔记记录的同步 ID 字段中,以保持本地数据与远程数据在同步方面的一致性。
|
|
|
values.put(NoteColumns.SYNC_ID, node.getLastModified());
|
|
|
// 通过 `ContentResolver` 的 `update` 方法,对本地数据库中对应的笔记记录进行更新操作,更新的记录通过 `ContentUris.withAppendedId` 方法构建的 URI(基于 `Notes.CONTENT_NOTE_URI` 并附上当前记录的 `ID`,即 `c.getLong(SqlNote.ID_COLUMN)`)来指定具体要更新的哪条记录,更新的数据就是前面准备好的 `values` 对象中的内容,后面两个 `null` 参数可能分别表示更新的条件(这里没有额外指定条件,即更新所有匹配的记录)和更新参数的选择(默认情况)。
|
|
|
mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
|
|
|
c.getLong(SqlNote.ID_COLUMN)), values, null, null);
|
|
|
} else {
|
|
|
// 如果根据 Gid 在 `mGTaskHashMap` 中没有找到对应的节点对象,说明存在本地的笔记记录在同步后没有对应的远程节点关联了,这可能是出现了数据不一致等问题,记录错误日志。
|
|
|
Log.e(TAG, "something is missed");
|
|
|
// 抛出异常,表明出现了同步后本地部分项目没有对应的 Gid 的异常情况,异常类型为 `ActionFailureException`,并附带相应的错误提示信息,方便上层调用者捕获并处理该异常情况。
|
|
|
throw new ActionFailureException(
|
|
|
"some local items don't have gid after sync");
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// 如果查询游标 `c` 为 `null`,说明查询本地笔记用于刷新同步 ID 的操作失败了,记录相应的警告日志,提示查询本地笔记失败的情况,但不会抛出异常中断整个流程(可能是希望后续可以再次尝试或者只是记录问题供排查)。
|
|
|
Log.w(TAG, "failed to query local note to refresh sync id");
|
|
|
}
|
|
|
} finally {
|
|
|
// 无论前面的查询操作是否成功,都要确保游标对象 `c` 被正确关闭,以释放相关的数据库资源,避免资源泄漏等问题。如果游标不为 `null`,则执行关闭操作,并将游标对象重新赋值为 `null`。
|
|
|
if (c!= null) {
|
|
|
c.close();
|
|
|
c = null;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public String getSyncAccount() {
|
|
|
// 该方法的功能是获取同步账户的名称信息,通过调用 `GTaskClient` 单例实例(通过 `getInstance` 方法获取单例对象)的 `getSyncAccount` 方法获取账户对象,然后再获取其名称属性(通过 `.name`)返回,具体 `GTaskClient` 类中相关方法和属性的实现取决于其具体的业务逻辑和设计,这里返回的就是当前用于同步操作的账户的名称字符串。
|
|
|
return GTaskClient.getInstance().getSyncAccount().name;
|
|
|
}
|
|
|
|
|
|
public void cancelSync() {
|
|
|
// 此方法用于取消同步操作,通过将 `mCancelled` 标志设置为 `true`,在其他相关的同步逻辑方法(比如前面的 `syncContent`、`syncFolder` 等涉及同步操作的方法)中会通过检查这个标志来判断是否需要提前终止同步流程,从而实现取消同步的功能。
|
|
|
mCancelled = true;
|
|
|
}
|
|
|
}
|