/* * 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; /** *

Google任务API客户端类。

*

设计意图:提供与Google任务API交互的核心功能,实现任务的创建、更新、删除等操作。

*

核心职责:

* *

关键关联:

* *

设计特点:

* */ public class GTaskClient { /** * 日志标签,用于调试和错误跟踪 */ private static final String TAG = GTaskClient.class.getSimpleName(); /** * Google任务的基本URL */ private static final String GTASK_URL = "https://mail.google.com/tasks/"; /** * Google任务的GET请求URL */ private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; /** * Google任务的POST请求URL */ private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; /** * GTaskClient的单例实例 */ private static GTaskClient mInstance = null; /** * HTTP客户端,用于执行与Google任务API的网络请求 */ private DefaultHttpClient mHttpClient; /** * 当前使用的GET请求URL */ private String mGetUrl; /** * 当前使用的POST请求URL */ private String mPostUrl; /** * Google任务客户端版本号 */ private long mClientVersion; /** * 登录状态标志 */ private boolean mLoggedin; /** * 最后一次登录时间 */ private long mLastLoginTime; /** * 请求操作ID,用于跟踪API请求 */ private int mActionId; /** * 当前登录的Google账号 */ private Account mAccount; /** * 批量更新的JSON数组,用于累积多次更新操作 */ 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的单例实例。

*

业务逻辑:使用双重检查锁定实现线程安全的单例模式。

* * @return GTaskClient的单例实例 */ public static synchronized GTaskClient getInstance() { if (mInstance == null) { mInstance = new GTaskClient(); } return mInstance; } /** *

登录Google账号并验证Google任务API。

*

业务逻辑:

* * * @param activity 当前活动的Activity,用于获取账号信息和显示认证界面 * @return true表示登录成功,false表示登录失败 */ public boolean login(Activity activity) { // we suppose that the cookie would expire after 5 minutes // 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; } if (mLoggedin) { Log.d(TAG, "already logged in"); return true; } mLastLoginTime = System.currentTimeMillis(); String authToken = loginGoogleAccount(activity, false); if (authToken == null) { Log.e(TAG, "login google account failed"); return false; } // login with custom domain if necessary 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 if (!mLoggedin) { mGetUrl = GTASK_GET_URL; mPostUrl = GTASK_POST_URL; if (!tryToLoginGtask(activity, authToken)) { return false; } } mLoggedin = true; return true; } /** *

登录Google账号并获取认证令牌。

*

业务逻辑:

* * * @param activity 当前活动的Activity,用于获取AccountManager * @param invalidateToken 是否使现有令牌失效 * @return 认证令牌字符串,如果获取失败则返回null */ 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, "there is no available google account"); 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, "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 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, "get auth token failed"); authToken = null; } return authToken; } /** *

尝试登录Google任务API。

*

业务逻辑:

* * * @param activity 当前活动的Activity,用于重新获取认证令牌 * @param authToken Google账号的认证令牌 * @return true表示登录成功,false表示登录失败 */ private boolean tryToLoginGtask(Activity activity, String authToken) { 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; } if (!loginGtask(authToken)) { Log.e(TAG, "login gtask failed"); return false; } } return true; } /** *

登录Google任务API的核心方法。

*

业务逻辑:

* * * @param authToken Google账号的认证令牌 * @return true表示登录成功,false表示登录失败 */ 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); // login gtask try { String loginUrl = mGetUrl + "?auth=" + authToken; HttpGet httpGet = new HttpGet(loginUrl); HttpResponse response = null; response = mHttpClient.execute(httpGet); // get the cookie now 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。

*

业务逻辑:返回当前操作ID并自增,用于跟踪API请求。

* * @return 当前操作ID */ private int getActionId() { return mActionId++; } /** *

创建HTTP POST请求。

*

业务逻辑:配置POST请求的URL和请求头。

* * @return 配置好的HttpPost对象 */ private HttpPost createHttpPost() { HttpPost httpPost = new HttpPost(mPostUrl); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); httpPost.setHeader("AT", "1"); return httpPost; } /** *

获取HTTP响应内容。

*

业务逻辑:

* * * @param entity HTTP响应实体 * @return 响应内容字符串 * @throws IOException 如果读取或解压响应内容时发生错误 */ private String getResponseContent(HttpEntity entity) throws IOException { String contentEncoding = null; if (entity.getContentEncoding() != null) { contentEncoding = entity.getContentEncoding().getValue(); Log.d(TAG, "encoding: " + contentEncoding); } InputStream input = entity.getContent(); if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { input = new GZIPInputStream(entity.getContent()); } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { Inflater inflater = new Inflater(true); input = new InflaterInputStream(entity.getContent(), inflater); } try { InputStreamReader isr = new InputStreamReader(input); BufferedReader br = new BufferedReader(isr); StringBuilder sb = new StringBuilder(); while (true) { String buff = br.readLine(); if (buff == null) { return sb.toString(); } sb = sb.append(buff); } } finally { input.close(); } } /** *

发送POST请求到Google任务API。

*

业务逻辑:

* * * @param js 要发送的JSON对象 * @return 响应的JSON对象 * @throws NetworkFailureException 如果网络请求失败 */ private JSONObject postRequest(JSONObject js) throws NetworkFailureException { if (!mLoggedin) { Log.e(TAG, "please login first"); throw new ActionFailureException("not logged in"); } HttpPost httpPost = createHttpPost(); try { LinkedList list = new LinkedList(); list.add(new BasicNameValuePair("r", js.toString())); UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); httpPost.setEntity(entity); // execute the post HttpResponse response = mHttpClient.execute(httpPost); String jsString = getResponseContent(response.getEntity()); return new JSONObject(jsString); } catch (ClientProtocolException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("postRequest failed"); } catch (IOException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("postRequest failed"); } catch (JSONException e) { 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"); } } /** *

创建新的Google任务。

*

业务逻辑:

*
    *
  • 提交当前累积的更新操作
  • *
  • 创建任务的JSON表示
  • *
  • 发送创建任务的POST请求
  • *
  • 从响应中获取新创建任务的GID并设置到任务对象中
  • *
* * @param task 要创建的任务对象 * @throws NetworkFailureException 如果网络请求失败 */ public void createTask(Task task) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); // action_list actionList.put(task.getCreateAction(getActionId())); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // client_version jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); // post JSONObject jsResponse = postRequest(jsPost); JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( GTaskStringUtils.GTASK_JSON_RESULTS).get(0); task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("create task: handing jsonobject failed"); } } /** *

创建新的Google任务列表。

*

业务逻辑:

*
    *
  • 提交当前累积的更新操作
  • *
  • 创建任务列表的JSON表示
  • *
  • 发送创建任务列表的POST请求
  • *
  • 从响应中获取新创建任务列表的GID并设置到任务列表对象中
  • *
* * @param tasklist 要创建的任务列表对象 * @throws NetworkFailureException 如果网络请求失败 */ public void createTaskList(TaskList tasklist) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); // action_list actionList.put(tasklist.getCreateAction(getActionId())); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // client version jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); // post JSONObject jsResponse = postRequest(jsPost); JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( GTaskStringUtils.GTASK_JSON_RESULTS).get(0); tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("create tasklist: handing jsonobject failed"); } } /** *

提交累积的更新操作。

*

业务逻辑:将累积在mUpdateArray中的所有更新操作发送到Google任务API。

* * @throws NetworkFailureException 如果网络请求失败 */ public void commitUpdate() throws NetworkFailureException { if (mUpdateArray != null) { try { JSONObject jsPost = new JSONObject(); // action_list jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); // client_version jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); postRequest(jsPost); mUpdateArray = null; } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("commit update: handing jsonobject failed"); } } } /** *

添加节点更新操作到累积列表。

*

业务逻辑:

*
    *
  • 如果累积的更新操作超过10个,先提交这些更新
  • *
  • 将节点的更新操作添加到累积列表中
  • *
* * @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 if (mUpdateArray != null && mUpdateArray.length() > 10) { commitUpdate(); } if (mUpdateArray == null) mUpdateArray = new JSONArray(); mUpdateArray.put(node.getUpdateAction(getActionId())); } } /** *

移动任务到新的父任务列表或在同一列表中重新排序。

*

业务逻辑:

*
    *
  • 提交当前累积的更新操作
  • *
  • 创建移动任务的JSON表示
  • *
  • 如果在同一列表内移动且不是第一个任务,设置前一个兄弟任务的ID
  • *
  • 发送移动任务的POST请求
  • *
* * @param task 要移动的任务 * @param preParent 任务的当前父任务列表 * @param curParent 任务的新父任务列表 * @throws NetworkFailureException 如果网络请求失败 */ public void moveTask(Task task, TaskList preParent, TaskList curParent) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); JSONObject action = new JSONObject(); // action_list action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); 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()); } action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); if (preParent != curParent) { // put the dest_list only if moving between tasklists action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); } actionList.put(action); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // client_version jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); postRequest(jsPost); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("move task: handing jsonobject failed"); } } /** *

删除节点。

*

业务逻辑:

*
    *
  • 提交当前累积的更新操作
  • *
  • 将节点标记为已删除
  • *
  • 创建删除节点的JSON表示
  • *
  • 发送删除节点的POST请求
  • *
  • 清空累积的更新列表
  • *
* * @param node 要删除的节点 * @throws NetworkFailureException 如果网络请求失败 */ public void deleteNode(Node node) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); // action_list node.setDeleted(true); actionList.put(node.getUpdateAction(getActionId())); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // client_version jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); postRequest(jsPost); mUpdateArray = null; } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("delete node: handing jsonobject failed"); } } /** *

获取所有的Google任务列表。

*

业务逻辑:

*
    *
  • 检查登录状态
  • *
  • 发送获取任务列表的GET请求
  • *
  • 解析响应内容,提取任务列表数据
  • *
* * @return 任务列表的JSON数组 * @throws NetworkFailureException 如果网络请求失败 */ public JSONArray getTaskLists() throws NetworkFailureException { if (!mLoggedin) { Log.e(TAG, "please login first"); throw new ActionFailureException("not logged in"); } try { HttpGet httpGet = new HttpGet(mGetUrl); HttpResponse response = null; response = mHttpClient.execute(httpGet); // get the task list 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); return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); } catch (ClientProtocolException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("gettasklists: httpget failed"); } catch (IOException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("gettasklists: httpget failed"); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("get task lists: handing jasonobject failed"); } } public JSONArray getTaskList(String listGid) throws NetworkFailureException { commitUpdate(); try { JSONObject jsPost = new JSONObject(); JSONArray actionList = new JSONArray(); JSONObject action = new JSONObject(); // action_list action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); actionList.put(action); jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); // client_version jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); JSONObject jsResponse = postRequest(jsPost); return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); } catch (JSONException e) { Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("get task list: handing jsonobject failed"); } } public Account getSyncAccount() { return mAccount; } public void resetUpdateArray() { mUpdateArray = null; } }