diff --git a/src/ActionFailureException.java b/src/ActionFailureException.java new file mode 100644 index 0000000..7af63a6 --- /dev/null +++ b/src/ActionFailureException.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; + +/** + * ActionFailureException 类是用于表示操作失败时抛出的异常。 + * 它继承自 RuntimeException,因此它是一个非检查型异常(unchecked exception), + * 这意味着在方法签名中不需要声明此异常可能会被抛出。 + */ +public class ActionFailureException extends RuntimeException { + + // 序列化版本唯一标识符,用于确保类的不同版本之间的兼容性。 + private static final long serialVersionUID = 4425249765923293627L; + + /** + * 构造一个没有详细信息消息和原因的 ActionFailureException。 + */ + public ActionFailureException() { + super(); + } + + /** + * 使用指定的详细信息消息构造一个新的 ActionFailureException。 + * @param paramString 提供更多关于异常的信息的消息字符串。 + */ + public ActionFailureException(String paramString) { + super(paramString); + } + + /** + * 使用指定的详细信息消息和原因构造一个新的 ActionFailureException。 + * @param paramString 提供更多关于异常的信息的消息字符串。 + * @param paramThrowable 导致当前异常的底层原因。 + */ + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} \ No newline at end of file diff --git a/src/GTaskASyncTask.java b/src/GTaskASyncTask.java new file mode 100644 index 0000000..c9d2171 --- /dev/null +++ b/src/GTaskASyncTask.java @@ -0,0 +1,174 @@ +/* + * 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任务(GTask)同步操作。 + * 它继承自 AsyncTask,以便在后台线程中执行耗时的网络操作, + * 并通过主线程更新用户界面或发送通知。 + */ +public class GTaskASyncTask extends AsyncTask { + + // 用于同步操作的通知ID,确保唯一性。 + private static final int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + /** + * 当同步完成时调用的接口。 + */ + public interface OnCompleteListener { + void onComplete(); + } + + // 上下文对象,用于访问应用资源和系统服务。 + private Context mContext; + + // 管理通知的服务。 + private NotificationManager mNotifiManager; + + // 负责管理与Google任务交互的任务管理器。 + private GTaskManager mTaskManager; + + // 同步完成后调用的监听器。 + private OnCompleteListener mOnCompleteListener; + + /** + * 构造一个新的 GTaskASyncTask 实例。 + * @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(); + } + + /** + * 更新进度信息。 + * @param message 进度消息文本。 + */ + public void publishProgess(String message) { + publishProgress(new String[]{message}); + } + + /** + * 显示一个通知给用户。 + * @param tickerId 通知栏提示文字的资源ID。 + * @param content 通知内容文本。 + */ + private void showNotification(int tickerId, String content) { + Notification notification = new Notification(R.drawable.notification, + mContext.getString(tickerId), + System.currentTimeMillis()); + notification.defaults = Notification.DEFAULT_LIGHTS; + notification.flags = Notification.FLAG_AUTO_CANCEL; + + // 根据同步结果选择不同的PendingIntent + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + pendingIntent = PendingIntent.getActivity(mContext, 0, + new Intent(mContext, NotesPreferenceActivity.class), 0); + } else { + pendingIntent = PendingIntent.getActivity(mContext, 0, + new Intent(mContext, NotesListActivity.class), 0); + } + + // 设置通知详情,并显示通知 + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, pendingIntent); + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } + + /** + * 在后台线程中执行的实际同步逻辑。 + */ + @Override + protected Integer doInBackground(Void... unused) { + // 发布登录进度信息 + publishProgess(mContext.getString(R.string.sync_progress_login, + NotesPreferenceActivity.getSyncAccountName(mContext))); + // 执行同步操作并返回状态码 + return mTaskManager.sync(mContext, this); + } + + /** + * 更新UI线程上的进度。 + */ + @Override + protected void onProgressUpdate(String... progress) { + // 显示同步中的通知 + showNotification(R.string.ticker_syncing, progress[0]); + // 如果上下文是GTaskSyncService,则广播进度信息 + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + } + } + + /** + * 同步完成后,在UI线程上执行此方法。 + */ + @Override + protected void onPostExecute(Integer result) { + // 根据同步结果显示不同通知 + switch(result) { + case GTaskManager.STATE_SUCCESS: + showNotification(R.string.ticker_success, + mContext.getString(R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); + break; + case GTaskManager.STATE_NETWORK_ERROR: + showNotification(R.string.ticker_fail, + mContext.getString(R.string.error_sync_network)); + break; + case GTaskManager.STATE_INTERNAL_ERROR: + showNotification(R.string.ticker_fail, + mContext.getString(R.string.error_sync_internal)); + break; + case GTaskManager.STATE_SYNC_CANCELLED: + showNotification(R.string.ticker_cancel, + mContext.getString(R.string.error_sync_cancelled)); + break; + } + + // 如果存在监听器,则启动新线程调用onComplete方法 + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + @Override + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} \ No newline at end of file diff --git a/src/GTaskClient.java b/src/GTaskClient.java new file mode 100644 index 0000000..8ae8950 --- /dev/null +++ b/src/GTaskClient.java @@ -0,0 +1,288 @@ +/* + * 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 Tasks 进行交互,主要包含任务和任务列表的创建、删除、修改等操作 +public class GTaskClient { + private static final String TAG = GTaskClient.class.getSimpleName(); + + // 定义 GTASK 的 URL 地址 + 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; // HTTP 客户端 + private String mGetUrl; // GET 请求 URL + private String mPostUrl; // POST 请求 URL + private long mClientVersion; // 客户端版本 + private boolean mLoggedin; // 登录状态 + private long mLastLoginTime; // 上次登录时间 + private int mActionId; // 操作 ID,用于唯一标识每个操作 + private Account mAccount; // 当前登录的 Google 帐号 + private JSONArray mUpdateArray; // 更新操作数组 + + // 私有化构造方法,避免外部实例化 + private GTaskClient() { + mHttpClient = null; + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; + mLoggedin = false; + mLastLoginTime = 0; + mActionId = 1; + mAccount = null; + mUpdateArray = null; + } + + // 单例模式获取 GTaskClient 实例 + public static synchronized GTaskClient getInstance() { + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + // 登录方法,返回是否成功 + public boolean login(Activity activity) { + // 每 5 分钟重新登录一次 + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // 如果切换了帐号,也需要重新登录 + if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity.getSyncAccountName(activity))) { + mLoggedin = false; + } + + if (mLoggedin) { + Log.d(TAG, "已经登录"); + return true; + } + + mLastLoginTime = System.currentTimeMillis(); + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "Google 帐号登录失败"); + return false; + } + + // 如果是自定义域名帐号,需要构建对应的 URL + 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; + } + } + + // 尝试使用 Google 官方 URL 登录 + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + + mLoggedin = true; + return true; + } + + // 获取授权令牌 + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + AccountManager accountManager = AccountManager.get(activity); + Account[] accounts = accountManager.getAccountsByType("com.google"); + + if (accounts.length == 0) { + Log.e(TAG, "没有可用的 Google 帐号"); + return null; + } + + // 根据设置中的帐号名称来选择帐户 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + if (account != null) { + mAccount = account; + } else { + Log.e(TAG, "无法找到相同名称的帐户"); + return null; + } + + // 获取授权令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, "goanna_mobile", null, activity, null, null); + try { + Bundle authTokenBundle = accountManagerFuture.getResult(); + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + Log.e(TAG, "获取授权令牌失败"); + authToken = null; + } + + return authToken; + } + + // 尝试登录 Google 任务 + private boolean tryToLoginGtask(Activity activity, String authToken) { + if (!loginGtask(authToken)) { + // 如果授权令牌过期,重新登录 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "Google 帐号登录失败"); + return false; + } + + if (!loginGtask(authToken)) { + Log.e(TAG, "登录 Google Tasks 失败"); + return false; + } + } + return true; + } + + // 通过授权令牌登录 Google Tasks + private boolean loginGtask(String authToken) { + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + mHttpClient = new DefaultHttpClient(httpParameters); + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // 登录 Google 任务 + try { + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = mHttpClient.execute(httpGet); + + // 获取 Cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "未找到授权 Cookie"); + } + + // 获取客户端版本 + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ");"; + + int beginIndex = resString.indexOf(jsBegin); + int endIndex = resString.indexOf(jsEnd); + if (beginIndex != -1 && endIndex != -1) { + String jsonString = resString.substring(beginIndex + jsBegin.length(), endIndex); + JSONObject jsonObject = new JSONObject(jsonString); + mClientVersion = jsonObject.optLong("clientVersion", -1); + } + return true; + } catch (Exception e) { + Log.e(TAG, "登录失败:" + e.getMessage()); + return false; + } + } + + // 获取 HTTP 响应内容 + private String getResponseContent(HttpEntity entity) throws IOException { + InputStream inputStream = entity.getContent(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line).append("\n"); + } + return stringBuilder.toString(); + } + + // 获取当前同步的 Google 帐号 + public Account getSyncAccount() { + return mAccount; + } + + // 获取已登录状态 + public boolean isLoggedin() { + return mLoggedin; + } +} diff --git a/src/GTaskManager.java b/src/GTaskManager.java new file mode 100644 index 0000000..6a3f39c --- /dev/null +++ b/src/GTaskManager.java @@ -0,0 +1,805 @@ +/* + * 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 Task的同步操作等相关功能 +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; + + // 关联的Activity,用于获取认证令牌等操作 + private Activity mActivity; + // 上下文对象 + private Context mContext; + // 内容解析器,用于操作内容提供器相关数据 + private ContentResolver mContentResolver; + // 标记是否正在同步 + private boolean mSyncing; + // 标记同步是否已取消 + private boolean mCancelled; + // 存储Google Task列表的哈希表,以任务列表的GID为键,对应的TaskList对象为值 + private HashMap mGTaskListHashMap; + // 存储Google Task节点的哈希表,以任务的GID为键,对应的Node对象为值 + private HashMap mGTaskHashMap; + // 存储元数据的哈希表,以相关GID为键,对应的MetaData对象为值 + private HashMap mMetaHashMap; + // 元数据列表对象 + private TaskList mMetaList; + // 存储本地要删除的记录的ID集合 + private HashSet mLocalDeleteIdMap; + // 存储Google任务的GID到本地记录ID的映射关系 + private HashMap mGidToNid; + // 存储本地记录ID到Google任务的GID的映射关系 + private HashMap mNidToGid; + + // 私有构造函数,用于初始化相关成员变量 + private GTaskManager() { + mSyncing = false; + mCancelled = false; + mGTaskListHashMap = new HashMap(); + mGTaskHashMap = new HashMap(); + mMetaHashMap = new HashMap(); + mMetaList = null; + mLocalDeleteIdMap = new HashSet(); + mGidToNid = new HashMap(); + mNidToGid = new HashMap(); + } + + // 获取GTaskManager的单例实例 + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + mInstance = new GTaskManager(); + } + return mInstance; + } + + // 设置关联的Activity上下文,主要用于获取认证令牌等操作 + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + mActivity = activity; + } + + // 执行同步操作的核心方法,根据不同情况进行与Google Task的同步流程 + public int sync(Context context, GTaskASyncTask asyncTask) { + if (mSyncing) { + Log.d(TAG, "Sync is in progress"); + return STATE_SYNC_IN_PROGRESS; + } + mContext = context; + mContentResolver = mContext.getContentResolver(); + mSyncing = true; + mCancelled = false; + // 清除之前同步相关的数据缓存 + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + + try { + GTaskClient client = GTaskClient.getInstance(); + client.resetUpdateArray(); + + // 登录Google Task,如果取消则不执行登录操作,登录失败抛出网络异常 + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // 发布初始化任务列表的进度提示 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // 发布正在同步内容的进度提示 + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + syncContent(); + } catch (NetworkFailureException e) { + Log.e(TAG, e.toString()); + return STATE_NETWORK_ERROR; + } catch (ActionFailureException e) { + Log.e(TAG, e.toString()); + return STATE_INTERNAL_ERROR; + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return STATE_INTERNAL_ERROR; + } finally { + // 无论如何,最终都要清除同步相关的数据缓存,并重置同步状态 + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + mSyncing = false; + } + + return mCancelled? STATE_SYNC_CANCELLED : STATE_SUCCESS; + } + + // 初始化Google Task列表,从Google获取任务列表信息并进行相关处理 + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) + return; + GTaskClient client = GTaskClient.getInstance(); + try { + // 获取Google Task列表的JSON数组表示 + JSONArray jsTaskLists = client.getTaskLists(); + + // 先初始化元数据列表,查找特定名称的元数据列表 + mMetaList = null; + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + mMetaList.setContentByRemoteJSON(object); + + // 加载元数据,获取对应任务列表下的元数据数组并逐个处理 + JSONArray jsMetas = client.getTaskList(gid); + for (int j = 0; j < jsMetas.length(); j++) { + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + metaData.setContentByRemoteJSON(object); + if (metaData.isWorthSaving()) { + mMetaList.addChildTask(metaData); + if (metaData.getGid()!= null) { + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // 如果元数据列表不存在,则创建一个新的元数据列表并上传到Google + if (mMetaList == null) { + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META); + GTaskClient.getInstance().createTaskList(mMetaList); + } + + // 初始化常规任务列表,遍历任务列表数组,创建并填充TaskList对象以及其包含的Task对象 + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) && + !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // 加载任务,获取对应任务列表下的任务数组并逐个处理 + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + } + } + + // 同步内容的主要方法,协调本地和远程数据的同步操作 + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + + mLocalDeleteIdMap.clear(); + + if (mCancelled) { + return; + } + + // 处理本地已删除的笔记,查询本地已删除笔记并与远程数据对比进行相应同步操作 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id=?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, null); + if (c!= null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); + } + + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + } + } else { + Log.w(TAG, "failed to query trash folder"); + } + } finally { + if (c!= null) { + c.close(); + c = null; + } + } + + // 同步文件夹相关数据 + syncFolder(); + + // 处理数据库中已存在的笔记,查询并根据情况确定同步类型,然后执行相应同步操作 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c!= null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + 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 note in database"); + } + + } finally { + if (c!= null) { + c.close(); + c = null; + } + } + + // 处理剩余的未处理的任务,进行添加本地任务的同步操作 + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + + // 如果未取消同步,清除本地已删除的记录 + // mCancelled可以被其他线程设置,所以需要逐个检查 + if (!mCancelled) { + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + } + + // 如果未取消同步,提交更新并刷新本地同步ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } + + // 同步文件夹的具体操作方法,处理不同类型文件夹(根文件夹、通话记录文件夹、本地现有文件夹、远程新增文件夹等)的同步 + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + + if (mCancelled) { + return; + } + + // 处理根文件夹的同步操作,查询根文件夹并根据情况进行相应同步操作 + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + if (c!= null) { + c.moveToNext(); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // 对于系统文件夹,仅在必要时更新远程名称 + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } else { + Log.w(TAG, "failed to query root folder"); + } + } finally { + if (c!= null) { + c.close(); + c = null; + } + } + + // 处理通话记录文件夹的同步操作,查询通话记录文件夹并根据情况进行相应同步操作 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + if (c!= null) { + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD } + } else { + Log.w(TAG, "failed to query call note folder"); + } + } finally { + if (c!= null) { + c.close(); + c = null; + } + } + + // 处理本地现有文件夹的同步操作,查询本地现有文件夹并根据情况确定同步类型,然后执行相应同步操作 + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c!= null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // 本地添加情况 + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // 远程删除情况 + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing folder"); + } + } finally { + if (c!= null) { + c.close(); + c = null; + } + } + + // 处理远程添加的文件夹,遍历远程任务列表哈希表,进行相应添加本地任务的同步操作 + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + } + + if (!mCancelled) + GTaskClient.getInstance().commitUpdate(); + } + + // 根据不同的同步类型执行具体的内容同步操作,如添加本地节点、添加远程节点、删除本地节点、删除远程节点等 + private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + MetaData meta; + switch (syncType) { + case Node.SYNC_ACTION_ADD_LOCAL: + addLocalNode(node); + break; + case Node.SYNC_ACTION_ADD_REMOTE: + addRemoteNode(node, c); + break; + case Node.SYNC_ACTION_DEL_LOCAL: + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta!= null) { + GTaskClient.getInstance().deleteNode(meta); + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + break; + case Node.SYNC_ACTION_DEL_REMOTE: + meta = mMetaHashMap.get(node.getGid()); + if (meta!= null) { + GTaskClient.getInstance().deleteNode(meta); + } + GTaskClient.getInstance().deleteNode(node); + break; + case Node.SYNC_ACTION_UPDATE_LOCAL: + updateLocalNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_REMOTE: + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_CONFLICT: + // 合并双方修改可能是个好主意,目前简单使用本地更新 + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: + break; + case Node.SYNC_ACTION_ERROR: + default: + throw new ActionFailureException("unkown sync action type"); + } + } + + // 添加本地节点的操作方法,根据节点类型(任务列表或普通任务)创建对应的本地记录并进行相关设置和更新 + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + return; + } + + 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 { + sqlNote = new SqlNote(mContext); + sqlNote.setContent(node.getLocalJSONFromContent()); + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + } + } else { + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // 如果ID已存在,需要创建一个新的,移除原来的ID + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // 如果数据ID已存在,需要创建一个新的,移除原来的ID + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js); + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // 创建本地节点,提交到本地数据库(不触发本地修改检查) + sqlNote.setGtaskId(node.getGid()); + sqlNote.commit(false); + + // 更新GID和本地记录ID的映射关系 + mGidToNid.put(node.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // 更新远程元数据相关信息 + updateRemoteMeta(node.getGid(), sqlNote); + } + + // 更新本地节点的操作方法,更新本地记录的内容、父节点等信息,并更新远程元数据相关信息 + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote; + // 根据传入的游标更新本地记录内容 + sqlNote = new SqlNote(mContext, c); + sqlNote.setContent(node.getLocalJSONFromContent()); + + Long parentId = (node instanceof Task)? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue()); + sqlNote.commit(true); + + // 更新远程元数据相关信息 + updateRemoteMeta(node.getGid(), sqlNote); + } + + // 添加远程节点的操作方法,根据节点类型(任务或任务列表)在远程进行相应创建操作,并更新本地记录相关信息和映射关系 + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + Node n; + + // 根据节点类型进行不同的远程更新操作 + if (sqlNote.isNoteType()) { + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + String parentGid = mNidToGid.get(sqlNote.getParentId()); + if (parentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot add remote task"); + } + mGTaskListHashMap.get(parentGid).addChildTask(task); + + GTaskClient.getInstance().createTask(task); + n = (Node) task; + + // 添加元数据相关更新 + updateRemoteMeta(task.getGid(), sqlNote); + } else { + TaskList tasklist = null; + + // 根据不同的本地记录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(); + + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + String gid = entry.getKey(); + TaskList list = entry.getValue(); + + if (list.getName().equals(folderName)) { + tasklist = list; + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + } + break; + } + } + + // 如果没有找到同名文件夹,则创建新的任务列表并上传到远程 + if (tasklist == null) { + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist; + } + + // 更新本地记录的GID等信息,并提交到本地数据库(先提交不触发本地修改检查,再提交触发检查) + sqlNote.setGtaskId(n.getGid()); + sqlNote.commit(false); + sqlNote.resetLocalModified(); + sqlNote.commit(true); + + // 更新GID和本地记录ID的映射关系 + mGidToNid.put(n.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), n.getGid()); + } + + // 更新远程节点的操作方法,更新远程节点内容、移动任务(如果需要)、清除本地修改标记等操作,并更新远程元数据相关信息 + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + + // 更新远程节点内容 + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + + // 更新远程元数据相关信息 + updateRemoteMeta(node.getGid(), sqlNote); + + // 如果是任务类型,检查是否需要移动任务到不同的任务列表 + if (sqlNote.isNoteType()) { + Task task = (Task) node; + TaskList preParentList = task.getParent(); + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); + if (curParentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot update remote task"); + } + TaskList curParentList = mGTaskListHashMap.get(curParentGid); + + if (preParentList!= curParentList) { + preParentList.removeChildTask(task); + curParentList.addChildTask(task); + GTaskClient.getInstance().moveTask(task, preParentList, curParentList); + } + } + + // 清除本地修改标记并提交到本地数据库 + sqlNote.resetLocalModified(); + sqlNote.commit(true); + } + + // 更新远程元数据的操作方法,根据情况更新已存在的元数据或者创建新的元数据并上传到远程 + 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的方法,重新获取最新的Google Task列表信息,然后更新本地记录的同步ID + private void refreshLocalSyncId() throws NetworkFailureException { + if (mCancelled) { + return; + } + + // 获取最新的Google Task列表相关数据,清除之前缓存 + mGTaskHashMap.clear(); + mGTaskListHashMap.clear(); + mMetaHashMap.clear(); + initGTaskList(); + + Cursor c = null; + 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) + }, NoteColumns.TYPE + " DESC"); + if (c!= null) { + while (c.moveToNext()) { + String gid = c.getString(SqlNote.GTASK_ID_COLUMN); + Node node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + ContentValues values = new ContentValues(); + values.put(NoteColumns.SYNC_ID, node.getLastModified()); + mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + c.getLong(SqlNote.ID_COLUMN)), values, null, null); + } else { + 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; + } + } + } + + // 获取同步账户名称的方法 + public String getSyncAccount() { + return GTaskClient.getInstance().getSyncAccount().name; + } + + // 取消同步的方法,设置同步已取消的标记 + public void cancelSync() { + mCancelled = true; + } +} \ No newline at end of file diff --git a/src/GTaskSyncService.java b/src/GTaskSyncService.java new file mode 100644 index 0000000..40d553b --- /dev/null +++ b/src/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任务(GTask)的同步操作。 + * 它提供启动和取消同步的方法,并在同步过程中广播进度更新。 + */ +public class GTaskSyncService extends Service { + + /** + * 表示同步动作类型的字符串常量名。 + */ + public final static String ACTION_STRING_NAME = "sync_action_type"; + + /** + * 同步开始的动作代码。 + */ + public final static int ACTION_START_SYNC = 0; + + /** + * 取消同步的动作代码。 + */ + public final static int ACTION_CANCEL_SYNC = 1; + + /** + * 无效或未定义的动作代码。 + */ + public final static int ACTION_INVALID = 2; + + /** + * 广播名称,用于发送同步状态和服务消息。 + */ + public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + + /** + * 广播中表示是否正在进行同步的键名。 + */ + public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + + /** + * 广播中表示同步进度信息的键名。 + */ + public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + + /** + * 当前执行的同步任务实例。 + */ + private static GTaskASyncTask mSyncTask = null; + + /** + * 当前同步任务的进度信息。 + */ + private static String mSyncProgress = ""; + + /** + * 开始同步操作。如果当前没有正在运行的同步任务,则创建并启动一个新的同步任务。 + */ + private void startSync() { + if (mSyncTask == null) { + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + @Override + public void onComplete() { + mSyncTask = null; + sendBroadcast(""); + stopSelf(); + } + }); + sendBroadcast(""); + mSyncTask.execute(); + } + } + + /** + * 取消正在进行的同步操作。 + */ + private void cancelSync() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + /** + * 当服务被创建时调用。 + */ + @Override + public void onCreate() { + super.onCreate(); + mSyncTask = null; + } + + /** + * 当服务接收到启动命令时调用。 + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Bundle bundle = intent.getExtras(); + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: + startSync(); + break; + case ACTION_CANCEL_SYNC: + cancelSync(); + break; + default: + // 忽略未知动作 + break; + } + return START_STICKY; // 如果进程被杀死,尝试重启服务 + } + return super.onStartCommand(intent, flags, startId); + } + + /** + * 当系统内存不足时调用,尝试释放资源。 + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + /** + * 返回绑定到此服务的IBinder对象,本服务不支持绑定,所以返回null。 + */ + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * 发送广播以通知其他组件当前同步的状态和进度。 + */ + public void sendBroadcast(String msg) { + mSyncProgress = msg; + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + sendBroadcast(intent); + } + + /** + * 静态方法,从Activity启动同步服务。 + */ + public static void startSync(Activity activity) { + GTaskManager.getInstance().setActivityContext(activity); + Intent intent = new Intent(activity, GTaskSyncService.class); + intent.putExtra(ACTION_STRING_NAME, ACTION_START_SYNC); + activity.startService(intent); + } + + /** + * 静态方法,通过上下文取消同步操作。 + */ + public static void cancelSync(Context context) { + Intent intent = new Intent(context, GTaskSyncService.class); + intent.putExtra(ACTION_STRING_NAME, ACTION_CANCEL_SYNC); + context.startService(intent); + } + + /** + * 检查是否有同步任务正在进行。 + */ + public static boolean isSyncing() { + return mSyncTask != null; + } + + /** + * 获取当前同步任务的进度信息。 + */ + public static String getProgressString() { + return mSyncProgress; + } +} \ No newline at end of file diff --git a/src/NetworkFailureException.java b/src/NetworkFailureException.java new file mode 100644 index 0000000..9e50613 --- /dev/null +++ b/src/NetworkFailureException.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; + +/** + * NetworkFailureException 类是用于表示网络操作失败时抛出的异常。 + * 它继承自 Exception,因此它是一个检查型异常(checked exception), + * 这意味着在方法签名中需要声明此异常可能会被抛出,以确保调用者处理或重新抛出该异常。 + */ +public class NetworkFailureException extends Exception { + + // 序列化版本唯一标识符,用于确保类的不同版本之间的兼容性。 + private static final long serialVersionUID = 2107610287180234136L; + + /** + * 构造一个新的 NetworkFailureException,不带任何详细信息消息或原因。 + */ + public NetworkFailureException() { + super(); + } + + /** + * 使用指定的详细信息消息构造一个新的 NetworkFailureException。 + * @param message 提供更多关于异常的信息的消息字符串。 + */ + public NetworkFailureException(String message) { + super(message); + } + + /** + * 使用指定的详细信息消息和原因构造一个新的 NetworkFailureException。 + * @param message 提供更多关于异常的信息的消息字符串。 + * @param cause 导致当前异常的底层原因。 + */ + public NetworkFailureException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file