From 7ea5536fc63b7d5ae03f2dc89f7b14467dd91aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E6=B7=91=E6=B6=B5?= <1508386176@qq.com> Date: Fri, 6 Dec 2024 21:20:44 +0800 Subject: [PATCH] Signed-off-by: --- GTaskASyncTask.java | 170 +++++++ GTaskClient.java | 807 +++++++++++++++++++++++++++++++ GTaskManager.java | 1065 +++++++++++++++++++++++++++++++++++++++++ GTaskSyncService.java | 198 ++++++++ 4 files changed, 2240 insertions(+) create mode 100644 GTaskASyncTask.java create mode 100644 GTaskClient.java create mode 100644 GTaskManager.java create mode 100644 GTaskSyncService.java diff --git a/GTaskASyncTask.java b/GTaskASyncTask.java new file mode 100644 index 0000000..ed52f7b --- /dev/null +++ b/GTaskASyncTask.java @@ -0,0 +1,170 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; +import net.micode.notes.ui.NotesPreferenceActivity; + +/* + * GTaskASyncTask类是一个异步任务类,用于执行Google任务的同步操作。 + * 该类继承自AsyncTask,并在后台线程中执行同步操作,同时在UI线程中更新进度和结果。 + */ +public class GTaskASyncTask extends AsyncTask { + + private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + public interface OnCompleteListener { //定义一个接口,用于在任务完成时通知监听器。 + void onComplete(); + } + + private Context mContext; + + private NotificationManager mNotifiManager; + + private GTaskManager mTaskManager; + + private OnCompleteListener mOnCompleteListener; +/* + * 构造函数,初始化上下文、通知管理器、任务管理器和完成监听器。 + * @param context 上下文 + * @param listener 完成监听器 + */ + public GTaskASyncTask(Context context, OnCompleteListener listener) { + // 初始化上下文 + mContext = context; + // 初始化完成监听器 + mOnCompleteListener = listener; + // 获取通知管理器 + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + // 获取任务管理器实例 + mTaskManager = GTaskManager.getInstance(); + } +/* + * 取消同步操作。 + */ + public void cancelSync() { + mTaskManager.cancelSync(); + } +/* + * 发布进度消息。 + */ + public void publishProgess(String message) { + publishProgress(new String[] { + message + }); + } +/* + * 显示通知。 + * @param tickerId 通知的提示文本ID + * @param content 通知的内容 + */ + private void showNotification(int tickerId, String content) { + // 创建一个新的Notification对象 + Notification notification = new Notification(R.drawable.notification, mContext + .getString(tickerId), System.currentTimeMillis()); + // 设置通知的默认灯光效果 + notification.defaults = Notification.DEFAULT_LIGHTS; + // 设置通知的标志,自动取消通知 + notification.flags = Notification.FLAG_AUTO_CANCEL; + // 创建PendingIntent,用于在用户点击通知时启动相应的Activity + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + // 如果通知不是成功通知,启动NotesPreferenceActivity + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + + } else { + // 如果通知是成功通知,启动NotesListActivity + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + // 设置通知的详细信息 + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, + pendingIntent); + // 发送通知 + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } +/* + * 在后台线程中执行同步操作。 + * @param unused 未使用的参数 + * @return 同步结果状态码 + */ + @Override + protected Integer doInBackground(Void... unused) { + // 发布登录进度消息 + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); + // 调用任务管理器的同步方法,并返回同步结果状态码 + return mTaskManager.sync(mContext, this); + } +/* + * 在UI线程中更新进度。 + * @param progress 进度消息 + */ + @Override + protected void onProgressUpdate(String... progress) { + // 显示同步进度通知 + showNotification(R.string.ticker_syncing, progress[0]); + // 如果上下文是GTaskSyncService的实例,发送广播 + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + } + } +/* + * 在UI线程中处理同步结果。 + * @param result 同步结果状态码 + */ + @Override + protected void onPostExecute(Integer result) { + // 根据同步结果状态码显示相应的通知 + if (result == GTaskManager.STATE_SUCCESS) { + // 如果同步成功,显示成功通知,并更新最后一次同步时间 + showNotification(R.string.ticker_success, mContext.getString( + R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); + } else if (result == GTaskManager.STATE_NETWORK_ERROR) { + // 如果同步失败,显示网络错误通知 + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network)); + } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { + // 如果同步失败,显示内部错误通知 + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal)); + } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { + // 如果同步被取消,显示取消通知 + showNotification(R.string.ticker_cancel, mContext + .getString(R.string.error_sync_cancelled)); + } + // 如果存在完成监听器,启动新线程调用onComplete方法 + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} diff --git a/GTaskClient.java b/GTaskClient.java new file mode 100644 index 0000000..d2d0669 --- /dev/null +++ b/GTaskClient.java @@ -0,0 +1,807 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/* + * GTaskClient类是一个用于与Google任务服务进行交互的客户端类。 + * 该类提供了登录、创建任务、创建任务列表、更新任务、移动任务、删除任务等功能。 + */ +public class GTaskClient { + private static final String TAG = GTaskClient.class.getSimpleName(); + + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; + + private DefaultHttpClient mHttpClient; + + private String mGetUrl; + + private String mPostUrl; + + private long mClientVersion; + + private boolean mLoggedin; + + private long mLastLoginTime; + + private int mActionId; + + private Account mAccount; + + private JSONArray mUpdateArray; +/* + * 私有构造函数,初始化成员变量。 + */ + private GTaskClient() { + mHttpClient = null; // 初始化HttpClient为null + mGetUrl = GTASK_GET_URL; // 初始化GET和POST请求的URL + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; // 初始化客户端版本号为-1 + mLoggedin = false; // 初始化登录状态为false + mLastLoginTime = 0; // 初始化最后一次登录时间为0 + mActionId = 1; // 初始化操作ID为1 + mAccount = null; // 初始化账户为null + mUpdateArray = null; // 初始化更新数组为null + } +/* + * 获取GTaskClient的单例实例。 + * @return GTaskClient的单例实例 + */ + public static synchronized GTaskClient getInstance() { + // 如果mInstance为null,创建一个新的GTaskClient实例 + if (mInstance == null) { + mInstance = new GTaskClient(); + } + // 返回GTaskClient的单例实例 + return mInstance; + } +/* + * 登录Google任务服务。 + * @param activity 当前活动 + * @return 是否登录成功 + */ + public boolean login(Activity activity) { + // we suppose that the cookie would expire after 5 minutes + // 假设cookie在5分钟后过期,需要重新登录 + // then we need to re-login + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // need to re-login after account switch + // 需要重新登录以切换账户 + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + // 如果已经登录,直接返回true + if (mLoggedin) { + Log.d(TAG, "already logged in"); + return true; + } + // 更新最后一次登录时间 + mLastLoginTime = System.currentTimeMillis(); + // 登录Google账户 + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // login with custom domain if necessary + // 如果账户不是gmail.com或googlemail.com,尝试使用自定义域名登录 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // try to login with google official url + // 尝试使用Google官方URL登录 + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + // 登录成功 + mLoggedin = true; + return true; + } +/* + * 登录Google账户并获取授权令牌。 + * @param activity 当前活动 + * @param invalidateToken 是否使授权令牌失效 + * @return 授权令牌,如果登录失败则返回null + */ + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + // 获取AccountManager实例 + AccountManager accountManager = AccountManager.get(activity); + // 获取所有Google账户 + Account[] accounts = accountManager.getAccountsByType("com.google"); + // 如果没有可用的Google账户,记录错误日志并返回null + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + // 获取设置中的同步账户名称 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + // 遍历所有Google账户,查找与设置中同步账户名称匹配的账户 + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + // 如果找到匹配的账户,将其赋值给mAccount + if (account != null) { + mAccount = account; + } else { + // 如果没有找到匹配的账户,记录错误日志并返回null + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // get the token now + // 获取授权令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + // 获取授权令牌的Bundle + Bundle authTokenBundle = accountManagerFuture.getResult(); + // 从Bundle中获取授权令牌 + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 如果需要使授权令牌失效,使令牌失效并重新获取 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + // 如果获取授权令牌失败,记录错误日志并返回null + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; // 返回授权令牌 + } +/* + * 尝试登录Google任务服务。 + * @param activity 当前活动 + * @param authToken 授权令牌 + * @return 是否登录成功 + */ + private boolean tryToLoginGtask(Activity activity, String authToken) { + // 尝试使用当前授权令牌登录Google任务服务 + if (!loginGtask(authToken)) { + // maybe the auth token is out of date, now let's invalidate the + // token and try again + // 如果登录失败,可能是授权令牌已过期,使令牌失效并重新获取 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + // 使用新的授权令牌再次尝试登录Google任务服务 + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } +/* + * 使用授权令牌登录Google任务服务。 + * @param authToken 授权令牌 + * @return 是否登录成功 + */ + private boolean loginGtask(String authToken) { + // 设置连接超时和套接字超时 + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + // 创建DefaultHttpClient实例 + mHttpClient = new DefaultHttpClient(httpParameters); + // 创建BasicCookieStore实例并设置到HttpClient中 + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + // 设置HttpClient不使用Expect-Continue + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // login gtask + // 登录Google任务服务 + try { + // 构建登录URL + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the cookie now + // 获取cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // get the client version + // 获取客户端版本 + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); + mClientVersion = js.getLong("v"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } catch (Exception e) { + // simply catch all exceptions + // 捕获所有异常 + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } +/* + * 获取操作ID并递增。 + * @return 当前操作ID + */ + private int getActionId() { + // 返回当前操作ID并递增 + return mActionId++; + } +/* + * 创建HttpPost请求。 + * @return 创建的HttpPost请求 + */ + private HttpPost createHttpPost() { + // 创建HttpPost请求,设置请求URL + HttpPost httpPost = new HttpPost(mPostUrl); + // 设置请求头,指定Content-Type为application/x-www-form-urlencoded,字符集为UTF-8 + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + // 设置请求头,指定AT为1 + return httpPost; + // 返回创建的HttpPost请求 + } +/* + * 从HttpEntity中获取响应内容并返回字符串形式。 + * 支持处理gzip和deflate压缩编码。 + * @param entity HTTP响应实体 + * @return 响应内容的字符串形式 + * @throws IOException 如果读取输入流时发生错误 + */ + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + // 检查HttpEntity是否包含Content-Encoding头部 + if (entity.getContentEncoding() != null) { + // 获取Content-Encoding的值 + contentEncoding = entity.getContentEncoding().getValue(); + // 记录日志,显示当前的Content-Encoding + Log.d(TAG, "encoding: " + contentEncoding); + } + // 获取HttpEntity的原始输入流 + InputStream input = entity.getContent(); + // 如果Content-Encoding是gzip,则使用GZIPInputStream解压缩 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + // 如果Content-Encoding是deflate,则使用InflaterInputStream解压缩 + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + // 创建InputStreamReader,用于将字节流转换为字符流 + InputStreamReader isr = new InputStreamReader(input); + // 创建BufferedReader,用于逐行读取字符流 + BufferedReader br = new BufferedReader(isr); + // 创建StringBuilder,用于存储读取到的内容 + StringBuilder sb = new StringBuilder(); + // 循环读取输入流中的每一行 + while (true) { + String buff = br.readLine(); + // 如果读取到null,表示已经到达流的末尾,返回StringBuilder中的内容 + if (buff == null) { + return sb.toString(); + } + // 将读取到的行追加到StringBuilder中 + sb = sb.append(buff); + } + } finally { + // 确保输入流在方法结束时被关闭 + input.close(); + } + } +/* + * 发送POST请求并返回响应的JSONObject。 + * @param js 要发送的请求数据,以JSONObject形式表示 + * @return 服务器响应的JSONObject + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如未登录或响应内容无法转换为JSONObject) + */ + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + // 检查用户是否已登录,如果未登录则抛出异常 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + // 创建HttpPost对象 + HttpPost httpPost = createHttpPost(); + try { + // 创建一个LinkedList来存储请求参数 + LinkedList list = new LinkedList(); + // 将请求数据(JSONObject)转换为字符串,并添加到请求参数列表中 + list.add(new BasicNameValuePair("r", js.toString())); + // 创建UrlEncodedFormEntity对象,用于将请求参数编码为表单格式 + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + // 将请求实体设置到HttpPost对象中 + httpPost.setEntity(entity); + // execute the post + // 执行POST请求 + HttpResponse response = mHttpClient.execute(httpPost); + // 获取响应内容并转换为字符串 + String jsString = getResponseContent(response.getEntity()); + // 将响应内容字符串转换为JSONObject并返回 + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + // 捕获并记录HTTP协议异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + // 捕获并记录IO异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("unable to convert response content to jsonobject"); + } catch (Exception e) { + // 捕获并记录其他异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("error occurs when posting request"); + } + } +/* + * 创建一个新的任务,并将其上传到服务器。 + * @param task 要创建的任务对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void createTask(Task task) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储任务的创建操作 + JSONArray actionList = new JSONArray(); + + // action_list + // 将任务的创建操作添加到actionList中 + actionList.put(task.getCreateAction(getActionId())); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + // 发送POST请求,并将响应内容转换为JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取结果数组,并获取第一个结果 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 从结果中获取新创建任务的ID,并设置到任务对象中 + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) {、 + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handing jsonobject failed"); + } + } +/* + * 创建一个新的任务列表,并将其上传到服务器。 + * @param tasklist 要创建的任务列表对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void createTaskList(TaskList tasklist) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储任务列表的创建操作 + JSONArray actionList = new JSONArray(); + + // action_list + // 将任务列表的创建操作添加到actionList中 + actionList.put(tasklist.getCreateAction(getActionId())); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + // 发送POST请求,并将响应内容转换为JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取结果数组,并获取第一个结果 + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 从结果中获取新创建任务列表的ID,并设置到任务列表对象中 + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handing jsonobject failed"); + } + } +/* + * 提交所有未完成的更新操作到服务器。 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void commitUpdate() throws NetworkFailureException { + // 检查是否有未完成的更新操作 + if (mUpdateArray != null) { + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + + // action_list + // 将未完成的更新操作数组添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求 + postRequest(jsPost); + // 提交成功后,清空更新操作数组 + mUpdateArray = null; + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("commit update: handing jsonobject failed"); + } + } + } +/* + * 将一个节点添加到更新操作数组中,并在必要时提交更新操作。 + * @param node 要添加的节点对象 + * @throws NetworkFailureException 如果网络请求失败 + */ + public void addUpdateNode(Node node) throws NetworkFailureException { + // 检查节点是否为空 + if (node != null) { + // too many update items may result in an error + // set max to 10 items + // 如果更新操作数组中已经有超过10个项目,则提交更新操作 + // 避免过多的更新操作导致错误 + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate(); + } + // 如果更新操作数组为空,则创建一个新的JSONArray + if (mUpdateArray == null) + mUpdateArray = new JSONArray(); + // 将节点的更新操作添加到更新操作数组中 + mUpdateArray.put(node.getUpdateAction(getActionId())); + } + } +/* + * 将任务从一个任务列表移动到另一个任务列表。 + * @param task 要移动的任务对象 + * @param preParent 任务的原始父任务列表 + * @param curParent 任务的新父任务列表 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void moveTask(Task task, TaskList preParent, TaskList curParent) + throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储任务的移动操作 + JSONArray actionList = new JSONArray(); + // 创建一个JSONObject,用于存储具体的移动操作 + JSONObject action = new JSONObject(); + + // action_list + // 设置移动操作的类型为"move" + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + // 设置移动操作的ID + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 设置要移动的任务的ID + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); + // 如果任务在同一个任务列表中移动,并且不是第一个任务,则设置前一个兄弟任务的ID + if (preParent == curParent && task.getPriorSibling() != null) { + // put prioring_sibing_id only if moving within the tasklist and + // it is not the first one + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } + // 设置任务的原始父任务列表的ID + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + // 设置任务的新父任务列表的ID + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); + // 如果任务在不同的任务列表之间移动,则设置目标任务列表的ID + if (preParent != curParent) { + // put the dest_list only if moving between tasklists + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + // 将移动操作添加到actionList中 + actionList.put(action); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求 + postRequest(jsPost); + + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("move task: handing jsonobject failed"); + } + } +/* + * 删除一个节点,并将其标记为已删除状态。 + * @param node 要删除的节点对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public void deleteNode(Node node) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储节点的删除操作 + JSONArray actionList = new JSONArray(); + + // action_list + // 将节点标记为已删除 + node.setDeleted(true); + // 获取节点的更新操作,并将其添加到actionList中 + actionList.put(node.getUpdateAction(getActionId())); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求 + postRequest(jsPost); + // 提交成功后,清空更新操作数组 + mUpdateArray = null; + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("delete node: handing jsonobject failed"); + } + } +/* + * 获取所有任务列表,并返回一个包含任务列表的JSONArray。 + * @return 包含任务列表的JSONArray + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如未登录或JSON处理失败) + */ + public JSONArray getTaskLists() throws NetworkFailureException { + // 检查用户是否已登录,如果未登录则抛出异常 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + try { + // 创建一个HttpGet对象,用于发送GET请求 + HttpGet httpGet = new HttpGet(mGetUrl); + HttpResponse response = null; + // 执行GET请求,并获取响应 + response = mHttpClient.execute(httpGet); + + // get the task list + // 获取响应内容并转换为字符串 + String resString = getResponseContent(response.getEntity()); + // 从响应内容中提取包含任务列表的JSON字符串 + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + // 将提取的JSON字符串转换为JSONObject + JSONObject js = new JSONObject(jsString); + // 从JSONObject中获取任务列表的JSONArray并返回 + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); + } catch (ClientProtocolException e) { + // 捕获并记录HTTP协议异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (IOException e) { + // 捕获并记录IO异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task lists: handing jasonobject failed"); + } + } +/* + * 获取指定任务列表中的所有任务,并返回一个包含任务的JSONArray。 + * @param listGid 任务列表的ID + * @return 包含任务的JSONArray + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果操作失败(例如JSON处理失败) + */ + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + // 提交更新操作 + commitUpdate(); + try { + // 创建一个新的JSONObject,用于存储POST请求的数据 + JSONObject jsPost = new JSONObject(); + // 创建一个JSONArray,用于存储获取任务列表的操作 + JSONArray actionList = new JSONArray(); + // 创建一个JSONObject,用于存储具体的获取操作 + JSONObject action = new JSONObject(); + + // action_list + // 设置获取操作的类型为"getall" + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + // 设置获取操作的ID + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 设置要获取的任务列表的ID + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + // 设置是否获取已删除的任务,这里设置为false + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + // 将获取操作添加到actionList中 + actionList.put(action); + // 将actionList添加到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本信息到jsPost中 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求,并将响应内容转换为JSONObject + JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取任务列表的JSONArray并返回 + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); + } catch (JSONException e) { + // 捕获并记录JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task list: handing jsonobject failed"); + } + } +/* + * 获取当前同步的账户。 + * @return 当前同步的账户对象 + */ + public Account getSyncAccount() { + return mAccount; + } +/* + * 重置更新操作数组。 + */ + public void resetUpdateArray() { + mUpdateArray = null; + } +} diff --git a/GTaskManager.java b/GTaskManager.java new file mode 100644 index 0000000..cf77515 --- /dev/null +++ b/GTaskManager.java @@ -0,0 +1,1065 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * GTaskManager 负责管理本地笔记数据库与 Google Tasks 之间的同步过程。 + */ +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.data.MetaData; +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.SqlNote; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +/* + * GTaskManager 负责管理本地笔记数据库与 Google Tasks 之间的同步过程。 + */ +public class GTaskManager { + private static final String TAG = GTaskManager.class.getSimpleName(); + // 同步状态常量 + public static final int STATE_SUCCESS = 0; + + public static final int STATE_NETWORK_ERROR = 1; + + public static final int STATE_INTERNAL_ERROR = 2; + + public static final int STATE_SYNC_IN_PROGRESS = 3; + + public static final int STATE_SYNC_CANCELLED = 4; + // 单例实例 + private static GTaskManager mInstance = null; + // 用于获取认证令牌的活动上下文 + private Activity mActivity; + // 应用程序上下文 + private Context mContext; + // 内容解析器用于数据库操作 + private ContentResolver mContentResolver; + // 同步状态标志 + private boolean mSyncing; + + private boolean mCancelled; + // 用于管理任务列表、节点、元数据和 GID 与 NID 映射的哈希表 + private HashMap mGTaskListHashMap; + + private HashMap mGTaskHashMap; + + private HashMap mMetaHashMap; + + private TaskList mMetaList; + + private HashSet mLocalDeleteIdMap; + + private HashMap mGidToNid; + + private HashMap mNidToGid; + + private GTaskManager() { + // 初始化同步状态标志,表示当前没有正在进行同步 + mSyncing = false; + // 初始化取消同步标志,表示当前没有取消同步 + mCancelled = false; + // 初始化用于存储 Google Task 列表的哈希表 + mGTaskListHashMap = new HashMap();、 + // 初始化用于存储 Google Task 节点的哈希表 + mGTaskHashMap = new HashMap(); + // 初始化用于存储元数据的哈希表 + mMetaHashMap = new HashMap(); + // 初始化元数据列表,初始值为 null + mMetaList = null; + // 初始化用于存储本地删除的笔记 ID 的集合 + mLocalDeleteIdMap = new HashSet(); + // 初始化用于存储 Google Task ID 到本地笔记 ID 映射的哈希表 + mGidToNid = new HashMap(); + // 初始化用于存储本地笔记 ID 到 Google Task ID 映射的哈希表 + mNidToGid = new HashMap(); + } +/* + * 获取 GTaskManager 的单例实例。 + * 该方法使用 synchronized 关键字确保线程安全,防止多个线程同时创建实例。 + * @return GTaskManager 的单例实例 + */ + public static synchronized GTaskManager getInstance() { + // 检查单例实例是否已经创建 + if (mInstance == null) { + // 如果未创建,则创建一个新的 GTaskManager 实例 + mInstance = new GTaskManager(); + } + return mInstance; // 返回单例实例 + } +/* + * 设置活动上下文,用于获取认证令牌。 + * 该方法使用 synchronized 关键字确保线程安全,防止多个线程同时修改 mActivity 变量。 + * @param activity 活动上下文 + */ + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + // 将传入的活动上下文赋值给 mActivity 变量 + mActivity = activity; + } +/* + * 同步本地笔记数据库与 Google Tasks。 + * 该方法负责初始化同步过程,处理同步过程中的各种异常,并返回同步结果状态。 + * @param context 应用程序上下文 + * @param asyncTask 异步任务对象,用于发布同步进度 + * @return 同步结果状态,可能的值为 STATE_SUCCESS, STATE_NETWORK_ERROR, STATE_INTERNAL_ERROR, STATE_SYNC_IN_PROGRESS, STATE_SYNC_CANCELLED + */ + public int sync(Context context, GTaskASyncTask asyncTask) { + // 检查是否已经有同步正在进行 + if (mSyncing) { + Log.d(TAG, "Sync is in progress"); + return STATE_SYNC_IN_PROGRESS; + } + // 设置应用程序上下文和内容解析器 + mContext = context; + mContentResolver = mContext.getContentResolver(); + // 标记同步正在进行 + mSyncing = true; + mCancelled = false; + // 清空所有数据结构 + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + + try { + // 获取 GTaskClient 实例并重置更新数组 + GTaskClient client = GTaskClient.getInstance(); + client.resetUpdateArray(); + + // login google task + // 登录 Google Task + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // get the task list from google + // 从 Google 获取任务列表 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // do content sync work + // 执行内容同步工作 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + syncContent(); + } catch (NetworkFailureException e) { + // 处理网络失败异常 + Log.e(TAG, e.toString()); + return STATE_NETWORK_ERROR; + } catch (ActionFailureException e) { + // 处理操作失败异常 + Log.e(TAG, e.toString()); + return STATE_INTERNAL_ERROR; + } catch (Exception e) { + // 处理其他异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return STATE_INTERNAL_ERROR; + } finally { + // 清空所有数据结构并标记同步结束 + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + mSyncing = false; + } + + return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; // 返回同步结果状态 + } +/* + * 初始化 Google Task 列表。 + * 该方法从 Google Task 获取任务列表,并初始化元数据列表和任务列表。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void initGTaskList() throws NetworkFailureException { + // 检查是否已经取消同步 + if (mCancelled) + return; + // 获取 GTaskClient 实例 + GTaskClient client = GTaskClient.getInstance(); + try { + // 从 Google Task 获取任务列表 + JSONArray jsTaskLists = client.getTaskLists(); + + // init meta list first + // 首先初始化元数据列表 + mMetaList = null; //初始化元数据列表,将其设置为 null。 + for (int i = 0; i < jsTaskLists.length(); i++) { + //遍历任务列表,逐个处理每个任务列表。 + JSONObject object = jsTaskLists.getJSONObject(i); + //获取当前任务列表的 JSONObject + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + //获取当前任务列表的 Google Task ID + //获取当前任务列表的名称。 + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + // 检查是否是元数据列表 + //如果是,则初始化元数据列表并加载元数据。 + if (name + .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + //创建一个新的 TaskList 对象作为元数据列表。 + mMetaList.setContentByRemoteJSON(object); + //使用远程 JSON 数据设置元数据列表的内容。 + + // load meta data + // 加载元数据 + JSONArray jsMetas = client.getTaskList(gid); + //获取元数据列表中的元数据,返回一个 JSONArray。 + for (int j = 0; j < jsMetas.length(); j++) { + //遍历元数据列表,逐个处理每个元数据。 + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + //创建一个新的 MetaData 对象 + metaData.setContentByRemoteJSON(object); + //使用远程 JSON 数据设置元数据的内容 + if (metaData.isWorthSaving()) { + //检查元数据是否值得保存。如果值得保存,则将其添加到元数据列表中,并更新 mMetaHashMap + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + //检查元数据的 Google Task ID 是否存在。如果存在,则将其添加到 mMetaHashMap 中 + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // create meta list if not existed + // 如果元数据列表不存在,则创建一个新的元数据列表 + if (mMetaList == null) { + //检查元数据列表是否为空。如果为空,则创建一个新的元数据列表并将其添加到 Google Task 中。 + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META); + //设置元数据列表的名称 + GTaskClient.getInstance().createTaskList(mMetaList); + //将元数据列表添加到 Google Task 中 + } + + // init task list + // 初始化任务列表 + for (int i = 0; i < jsTaskLists.length(); i++) { + //再次遍历任务列表,逐个处理每个任务列表 + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + // 检查是否是 MIUI 文件夹前缀的任务列表 + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) + && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META)) { + //检查当前任务列表是否是 MIUI 文件夹前缀的任务列表,并且不是元数据列表。如果是,则初始化任务列表并加载任务 + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // load tasks + // 加载任务 + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + //遍历任务列表,逐个处理每个任务。 + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + //检查任务是否值得保存。如果值得保存,则将其添加到任务列表中,并更新 mGTaskHashMap。 + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + // 处理 JSON 解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + } + } +/* + * 同步内容。 + * 该方法负责同步本地数据库中的笔记与 Google Tasks 中的任务。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + // 清空本地删除的笔记 ID 集合 + mLocalDeleteIdMap.clear(); + // 检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + if (mCancelled) { + return; + } + + // for local deleted note + // 处理本地删除的笔记 + //使用 try-finally 结构确保在查询结束后关闭游标。 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id=?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, null); + //查询本地删除的笔记。查询条件为 type 不等于 Notes.TYPE_SYSTEM 且 parent_id 等于 Notes.ID_TRASH_FOLER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个笔记。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //获取当前笔记的 Google Task ID。 + node = mGTaskHashMap.get(gid); + //从 mGTaskHashMap 中获取对应的节点。 + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并执行远程删除操作。 + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); + //执行远程删除操作。 + } + + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + //将当前笔记的本地 ID 添加到 mLocalDeleteIdMap 中。 + } + } else { + Log.w(TAG, "failed to query trash folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // sync folder first + syncFolder(); + + // for note existing in database + // 处理数据库中存在的笔记 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + //查询数据库中存在的笔记。查询条件为 type 等于 Notes.TYPE_NOTE 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个笔记。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //获取当前笔记的 Google Task ID。 + node = mGTaskHashMap.get(gid); + //从 mGTaskHashMap 中获取对应的节点。 + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射,然后获取同步操作类型。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + //更新 Google Task ID 到本地笔记 ID 的映射。 + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + //更新本地笔记 ID 到 Google Task ID 的映射。 + syncType = node.getSyncAction(c); + //获取当前笔记的同步操作类型。 + } else { + //如果节点不存在,则根据 Google Task ID 是否为空来确定同步操作类型。 + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + // 本地添加 + //检查 Google Task ID 是否为空。如果为空,则表示本地添加操作。 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + //设置同步操作类型为远程添加。 + } else { + // remote delete + // 远程删除 + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c);//执行同步操作。 + } + } else { + Log.w(TAG, "failed to query existing note in database"); + } + + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // go through remaining items + // 处理剩余的项目 + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + //获取 mGTaskHashMap 的迭代器。 + while (iter.hasNext()) { + //遍历 mGTaskHashMap,逐个处理剩余的项目。 + Map.Entry entry = iter.next(); + //获取当前的键值对。 + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + //执行本地添加操作 + } + + // mCancelled can be set by another thread, so we neet to check one by + // one + // clear local delete table + // 检查是否已经取消同步 + if (!mCancelled) { + // 批量删除本地删除的笔记 + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + //批量删除本地删除的笔记。如果删除失败,则抛出 ActionFailureException。 + } + + // refresh local sync id + // 刷新本地同步 ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } +/* + * 同步文件夹。 + * 该方法负责同步本地数据库中的文件夹与 Google Tasks 中的任务列表。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + if (mCancelled) { + return; + } + + // for root folder + //处理根文件夹。 + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + //查询根文件夹。查询条件为 _id 等于 Notes.ID_ROOT_FOLDER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则处理查询结果。 + c.moveToNext(); + //移动游标到下一行。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + //获取当前文件夹的 Google Task ID。 + node = mGTaskHashMap.get(gid); + //从 mGTaskHashMap 中获取对应的节点。 + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + //更新 Google Task ID 到本地笔记 ID 的映射。 + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // for system folder, only update remote name if necessary + //更新本地笔记 ID 到 Google Task ID 的映射。 + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + //检查节点名称是否需要更新。如果需要更新,则执行远程更新操作。 + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);//执行远程更新操作。 + } + } else { + Log.w(TAG, "failed to query root folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for call-note folder + //处理通话记录文件夹。 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + //查询通话记录文件夹。查询条件为 _id 等于 Notes.ID_CALL_RECORD_FOLDER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则处理查询结果。 + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); + // for system folder, only update remote name if + // necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + //检查节点名称是否需要更新。如果需要更新,则执行远程更新操作。 + } else { + //如果节点不存在,则执行远程添加操作。 + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } + } else { + Log.w(TAG, "failed to query call note folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for local existing folders + //处理本地存在的文件夹。 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + //查询本地存在的文件夹。查询条件为 type 等于 Notes.TYPE_FOLDER 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个文件夹。 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新 mGidToNid 和 mNidToGid 映射,然后获取同步操作类型。 + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + //如果节点不存在,则根据 Google Task ID 是否为空来确定同步操作类型。 + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + //检查 Google Task ID 是否为空。如果为空,则表示本地添加操作 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + //如果 Google Task ID 不为空,则表示远程删除操作。 + // remote delete + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for remote add folders + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + //获取 mGTaskListHashMap 的迭代器。 + while (iter.hasNext()) { + //遍历 mGTaskListHashMap,逐个处理远程添加的文件夹。 + Map.Entry entry = iter.next(); + //获取当前的键值对 + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + //检查 mGTaskHashMap 中是否包含该节点。如果包含,则从 mGTaskHashMap 中移除该节点,并执行本地添加操作。 + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + } + + if (!mCancelled) //检查是否已经取消同步。如果没有取消,则提交更新。 + GTaskClient.getInstance().commitUpdate(); + } +/* + * 执行内容同步操作。 + * 该方法根据同步类型执行相应的同步操作。 + * @param syncType 同步类型 + * @param node 节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException { + // 检查是否已经取消同步 + if (mCancelled) { + return; + } + + MetaData meta; + //根据同步类型执行相应的同步操作。 + switch (syncType) { + case Node.SYNC_ACTION_ADD_LOCAL: + // 本地添加操作 + addLocalNode(node); + break; + case Node.SYNC_ACTION_ADD_REMOTE: + // 远程添加操作 + addRemoteNode(node, c); + break; + case Node.SYNC_ACTION_DEL_LOCAL: + // 本地删除操作。从 mMetaHashMap 中获取元数据。如果元数据存在,则调用 GTaskClient.getInstance().deleteNode(meta) 方法删除元数据。将本地笔记 ID 添加到 mLocalDeleteIdMap 中。 + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + break; + case Node.SYNC_ACTION_DEL_REMOTE: + // 远程删除操作。从 mMetaHashMap 中获取元数据。如果元数据存在,则调用 GTaskClient.getInstance().deleteNode(meta) 方法删除元数据。调用 GTaskClient.getInstance().deleteNode(node) 方法删除节点。 + meta = mMetaHashMap.get(node.getGid()); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + GTaskClient.getInstance().deleteNode(node); + break; + case Node.SYNC_ACTION_UPDATE_LOCAL: + // 本地更新操作 + updateLocalNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_REMOTE: + // 远程更新操作 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_CONFLICT: + // merging both modifications maybe a good idea + // right now just use local update simply + // 更新冲突操作 + // 合并两个修改可能是一个好主意 + // 现在只是简单地使用本地更新 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: + // 无操作 + break; + case Node.SYNC_ACTION_ERROR: + default: + // 未知同步操作类型 + throw new ActionFailureException("unkown sync action type"); + } + } +/* + * 添加本地节点。 + * 该方法负责将远程节点添加到本地数据库中。 + * @param node 远程节点对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote;//声明一个 SqlNote 变量,用于存储本地笔记。 + if (node instanceof TaskList) { + //检查节点是否是任务列表。如果是任务列表,则根据节点的名称创建相应的本地笔记。 + if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { + //检查节点名称是否是根文件夹。如果是根文件夹,则创建根文件夹的本地笔记。 + sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); + } else if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { + //检查节点名称是否是通话记录文件夹。如果是通话记录文件夹,则创建通话记录文件夹的本地笔记。 + sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); + } else { + //其他文件夹。创建新的本地笔记,并设置内容和父节点 ID。 + sqlNote = new SqlNote(mContext); + sqlNote.setContent(node.getLocalJSONFromContent()); + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + } + } else { + //如果是任务。创建新的本地笔记,并设置内容和父节点 ID。 + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + //处理 JSON 解析异常。 + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + //检查是否包含笔记信息。如果包含笔记信息,则检查笔记 ID 是否可用。 + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + //检查笔记信息是否包含 ID。如果包含 ID,则检查 ID 是否可用。 + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // the id is not available, have to create a new one + //检查笔记 ID 是否存在于本地数据库中。如果存在,则移除笔记 ID。 + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + //检查是否包含数据信息。如果包含数据信息,则检查数据 ID 是否可用。 + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + for (int i = 0; i < dataArray.length(); i++) { + //遍历数据信息,逐个处理每个数据。 + JSONObject data = dataArray.getJSONObject(i); + //获取当前数据信息。 + if (data.has(DataColumns.ID)) { + //检查数据信息是否包含 ID。如果包含 ID,则检查 ID 是否可用。 + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // the data id is not available, have to create + // a new one + //检查数据 ID 是否存在于本地数据库中。如果存在,则移除数据 ID。 + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js);//设置本地笔记的内容。 + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + //获取父节点的本地 ID。 + if (parentId == null) { + //检查父节点的本地 ID 是否存在。如果不存在,则抛出 ActionFailureException 异常。 + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // create the local node + sqlNote.setGtaskId(node.getGid());//更新 Google Task ID 到本地笔记 ID 的映射。 + sqlNote.commit(false); + + // update gid-nid mapping + mGidToNid.put(node.getGid(), sqlNote.getId());//更新本地笔记 ID 到 Google Task ID 的映射。 + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote);//更新元数据。 + } +/* + * 更新本地节点。 + * 该方法负责将远程节点的更新同步到本地数据库中。 + * @param node 远程节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote; + // update the note locally + //声明一个 SqlNote 变量,用于存储本地笔记。 + sqlNote = new SqlNote(mContext, c); + //创建一个新的 SqlNote 对象,并使用游标 c 初始化。 + sqlNote.setContent(node.getLocalJSONFromContent()); + //设置本地笔记的内容为远程节点的本地 JSON 内容。 + Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + //获取父节点的本地 ID。如果节点是任务,则从 mGidToNid 映射中获取父节点的本地 ID。如果节点不是任务,则将父节点 ID 设置为根文件夹的 ID。 + if (parentId == null) { + //检查父节点的本地 ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue());//设置本地笔记的父节点 ID。 + sqlNote.commit(true);//提交本地笔记的更新。 + + // update meta info + //更新元数据信息。 + updateRemoteMeta(node.getGid(), sqlNote); + } +/* + * 添加远程节点。 + * 该方法负责将本地笔记添加到 Google Tasks 中。 + * @param node 远程节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + //创建一个新的 SqlNote 对象,并使用游标 c 初始化。 + Node n; + //声明一个 Node 变量,用于存储远程节点 + + // update remotely + if (sqlNote.isNoteType()) { + //检查本地笔记是否是笔记类型。如果是笔记类型,则创建一个新的 Task 对象,并设置内容。 + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + String parentGid = mNidToGid.get(sqlNote.getParentId()); + if (parentGid == null) { + //检查父任务列表的 Google Task ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot add remote task"); + } + mGTaskListHashMap.get(parentGid).addChildTask(task); + //将任务添加到父任务列表中。 + + GTaskClient.getInstance().createTask(task); + //创建远程任务。 + n = (Node) task; + //将 Task 对象赋值给 Node 变量。 + + // add meta + //添加元数据。 + updateRemoteMeta(task.getGid(), sqlNote); + } else { + //如果是文件夹类型。创建一个新的 TaskList 对象,并设置内容。 + TaskList tasklist = null; + + // we need to skip folder if it has already existed + String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;//初始化文件夹名称 + if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) + //检查本地笔记的 ID 是否是根文件夹的 ID。如果是根文件夹,则设置文件夹名称为默认文件夹名称。 + folderName += GTaskStringUtils.FOLDER_DEFAULT; + else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) + //检查本地笔记的 ID 是否是通话记录文件夹的 ID。如果是通话记录文件夹,则设置文件夹名称为通话记录文件夹名称。 + folderName += GTaskStringUtils.FOLDER_CALL_NOTE; + else + //其他文件夹。设置文件夹名称为本地笔记的片段。 + folderName += sqlNote.getSnippet(); + + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + //遍历 mGTaskListHashMap,查找匹配的文件夹。 + Map.Entry entry = iter.next(); + String gid = entry.getKey(); + TaskList list = entry.getValue(); + + if (list.getName().equals(folderName)) { + //检查任务列表的名称是否与文件夹名称匹配。如果匹配,则将任务列表赋值给 tasklist 变量,并从 mGTaskHashMap 中移除该节点。 + tasklist = list; + if (mGTaskHashMap.containsKey(gid)) { + //检查 mGTaskHashMap 中是否包含该节点。如果包含,则从 mGTaskHashMap 中移除该节点。 + mGTaskHashMap.remove(gid); + } + break; + } + } + + // no match we can add now + if (tasklist == null) { + //如果没有匹配的文件夹,则可以添加。创建一个新的 TaskList 对象,并设置内容。 + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist;//将 TaskList 对象赋值给 Node 变量。 + } + + // update local note + sqlNote.setGtaskId(n.getGid());//设置本地笔记的 Google Task ID + sqlNote.commit(false);//提交本地笔记的更新。 + sqlNote.resetLocalModified();//重置本地笔记的修改标志。 + sqlNote.commit(true);//重置本地笔记的修改标志。 + + // gid-id mapping + mGidToNid.put(n.getGid(), sqlNote.getId());//更新 Google Task ID 到本地笔记 ID 的映射。 + mNidToGid.put(sqlNote.getId(), n.getGid());//更新本地笔记 ID 到 Google Task ID 的映射。 + } +/* + * 更新远程节点。 + * 该方法负责将本地笔记的更新同步到 Google Tasks 中。 + * @param node 远程节点对象 + * @param c 游标对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + //检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c);//创建一个新的 SqlNote 对象,并使用游标 c 初始化。 + + // update remotely + //使用本地笔记的内容更新远程节点的内容。 + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + //将更新后的节点添加到 GTaskClient 的更新列表中。 + + // update meta + //更新元数据。 + updateRemoteMeta(node.getGid(), sqlNote); + + // move task if necessary + if (sqlNote.isNoteType()) { + //检查本地笔记是否是笔记类型。如果是笔记类型,则处理任务的移动 + Task task = (Task) node; + TaskList preParentList = task.getParent(); + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); + if (curParentGid == null) { + //检查父任务列表的 Google Task ID 是否存在。如果不存在,则记录错误日志并抛出 ActionFailureException 异常。 + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot update remote task"); + } + TaskList curParentList = mGTaskListHashMap.get(curParentGid); + //获取当前父任务列表。 + if (preParentList != curParentList) { + //检查任务的当前父任务列表是否与当前父任务列表不同。如果不同,则从当前父任务列表中移除任务,并将其添加到新的父任务列表中。 + preParentList.removeChildTask(task); + curParentList.addChildTask(task); + GTaskClient.getInstance().moveTask(task, preParentList, curParentList); + } + } + + // clear local modified flag + sqlNote.resetLocalModified();//清除本地笔记的修改标志。 + sqlNote.commit(true);//提交本地笔记的更新。 + } +/* + * 更新远程元数据。 + * 该方法负责将本地笔记的元数据同步到 Google Tasks 中。 + * @param gid Google Task ID + * @param sqlNote 本地笔记对象 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException { + // 检查本地笔记是否为空且是笔记类型 + if (sqlNote != null && sqlNote.isNoteType()) { + // 从元数据哈希表中获取元数据 + MetaData metaData = mMetaHashMap.get(gid); + if (metaData != null) { + // 如果元数据存在,则更新元数据 + metaData.setMeta(gid, sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(metaData); + } else { + // 如果元数据不存在,则创建新的元数据 + metaData = new MetaData(); + metaData.setMeta(gid, sqlNote.getContent()); + mMetaList.addChildTask(metaData); + mMetaHashMap.put(gid, metaData); + GTaskClient.getInstance().createTask(metaData); + } + } + } +/* + * 刷新本地同步 ID。 + * 该方法负责刷新本地笔记的同步 ID,确保本地笔记的同步 ID 与 Google Tasks 中的最新数据一致。 + * @throws NetworkFailureException 如果网络操作失败 + */ + private void refreshLocalSyncId() throws NetworkFailureException { + // 检查是否已经取消同步。如果 mCancelled 为 true,则直接返回,不执行后续操作。 + if (mCancelled) { + return; + } + + // get the latest gtask list + mGTaskHashMap.clear();//清空 mGTaskHashMap + mGTaskListHashMap.clear();//清空 mGTaskListHashMap。 + mMetaHashMap.clear();//清空 mMetaHashMap。 + initGTaskList();//初始化 Google Task 列表。 + + Cursor c = null;//声明一个游标变量 c,用于存储查询结果。 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id<>?)", new String[] { + //查询本地笔记。查询条件为 type 不等于 Notes.TYPE_SYSTEM 且 parent_id 不等于 Notes.ID_TRASH_FOLER。 + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + //检查查询结果是否为空。如果不为空,则遍历查询结果。 + while (c.moveToNext()) { + //遍历查询结果,逐个处理每个笔记。 + String gid = c.getString(SqlNote.GTASK_ID_COLUMN); + Node node = mGTaskHashMap.get(gid); + if (node != null) { + //检查节点是否存在。如果存在,则从 mGTaskHashMap 中移除该节点,并更新本地笔记的同步 ID。 + mGTaskHashMap.remove(gid); + ContentValues values = new ContentValues(); + values.put(NoteColumns.SYNC_ID, node.getLastModified()); + mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + c.getLong(SqlNote.ID_COLUMN)), values, null, null); + } else { + //如果节点不存在,则记录错误日志并抛出 ActionFailureException 异常。 + Log.e(TAG, "something is missed"); + throw new ActionFailureException( + "some local items don't have gid after sync"); + } + } + } else { + Log.w(TAG, "failed to query local note to refresh sync id"); + } + } finally { + if (c != null) { + //检查游标是否为空。如果不为空,则关闭游标。 + c.close(); + c = null; + } + } + } +/* + * 获取同步账户名称。 + * 该方法返回当前用于同步的 Google 账户名称。 + * @return 同步账户名称 + */ + public String getSyncAccount() { + // 获取 GTaskClient 的单例实例,并返回同步账户的名称 + return GTaskClient.getInstance().getSyncAccount().name; + } +/* + * 取消同步。 + * 该方法用于设置取消同步标志,以便在同步过程中停止同步操作。 + */ + public void cancelSync() { + // 设置取消同步标志为 true + mCancelled = true; + } +} diff --git a/GTaskSyncService.java b/GTaskSyncService.java new file mode 100644 index 0000000..7ba4834 --- /dev/null +++ b/GTaskSyncService.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +/* + * GTaskSyncService类是一个服务类,用于处理Google任务的同步操作。 + * 该类继承自Service,并在后台执行同步任务。 + */ +public class GTaskSyncService extends Service { + public final static String ACTION_STRING_NAME = "sync_action_type"; + + public final static int ACTION_START_SYNC = 0; + + public final static int ACTION_CANCEL_SYNC = 1; + + public final static int ACTION_INVALID = 2; + + public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + + public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + + public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + + private static GTaskASyncTask mSyncTask = null; + + private static String mSyncProgress = ""; +/* + * 启动同步操作。 + */ + private void startSync() { + // 如果同步任务尚未启动 + if (mSyncTask == null) { + // 创建一个新的GTaskASyncTask对象 + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + public void onComplete() { + // 同步任务完成后,将mSyncTask置为null + mSyncTask = null; + // 发送广播通知同步完成 + sendBroadcast(""); + // 停止服务 + stopSelf(); + } + }); + // 发送广播通知同步开始 + sendBroadcast(""); + // 执行同步任务 + mSyncTask.execute(); + } + } +/* + * 取消同步操作。 + */ + private void cancelSync() { + // 如果同步任务正在执行 + if (mSyncTask != null) { + // 调用cancelSync方法取消同步 + mSyncTask.cancelSync(); + } + } +/* + * 服务创建时调用,初始化同步任务。 + */ + @Override + public void onCreate() { + // 将同步任务置为null + mSyncTask = null; + } +/* + * 服务启动时调用,处理启动和取消同步的命令。 + * @param intent 启动服务的意图 + * @param flags 启动标志 + * @param startId 启动ID + * @return 服务启动模式 + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // 获取意图中的额外数据 + Bundle bundle = intent.getExtras(); + // 如果额外数据不为空且包含ACTION_STRING_NAME + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据ACTION_STRING_NAME的值执行相应的操作 + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: + // 启动同步操作 + startSync(); + break; + case ACTION_CANCEL_SYNC: + // 取消同步操作 + cancelSync(); + break; + default: + break; + } + // 返回START_STICKY,表示服务在异常终止后会自动重启 + return START_STICKY; + } + // 如果额外数据为空或不包含ACTION_STRING_NAME,调用父类的onStartCommand方法 + return super.onStartCommand(intent, flags, startId); + } +/* + * 内存不足时调用,取消同步任务。 + */ + @Override + public void onLowMemory() { + // 如果同步任务正在执行 + if (mSyncTask != null) { + // 调用cancelSync方法取消同步 + mSyncTask.cancelSync(); + } + } +/* + * 绑定服务时调用,返回null。 + * @param intent 绑定服务的意图 + * @return null + */ + public IBinder onBind(Intent intent) { + // 返回null,表示不支持绑定服务 + return null; + } +/* + * 发送广播,通知同步状态和进度。 + * @param msg 进度消息 + */ + public void sendBroadcast(String msg) { + // 更新同步进度字符串 + mSyncProgress = msg; + // 创建一个新的Intent对象,用于发送广播 + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + // 添加是否正在同步的标志 + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + // 添加进度消息 + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + // 发送广播 + sendBroadcast(intent); + } +/* + * 启动同步操作的静态方法。 + * @param activity 启动同步的Activity + */ + public static void startSync(Activity activity) { + // 设置活动上下文 + GTaskManager.getInstance().setActivityContext(activity); + // 创建一个新的Intent对象,用于启动服务 + Intent intent = new Intent(activity, GTaskSyncService.class); + // 添加启动同步的命令 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + // 启动服务 + activity.startService(intent); + } +/* + * 取消同步操作的静态方法。 + * @param context 取消同步的上下文 + */ + public static void cancelSync(Context context) { + // 创建一个新的Intent对象,用于启动服务 + Intent intent = new Intent(context, GTaskSyncService.class); + // 添加取消同步的命令 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + // 启动服务 + context.startService(intent); + } +/* + * 检查是否正在同步。 + * @return 如果正在同步,返回true;否则返回false + */ + public static boolean isSyncing() { + // 如果mSyncTask不为null,表示正在同步 + return mSyncTask != null; + } +/* + * 获取同步进度字符串。 + * @return 同步进度字符串 + */ + public static String getProgressString() { + // 返回当前的同步进度字符串 + return mSyncProgress; + } +}