You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rass/gtask/remote/GTaskClient.java

425 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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服务进行交互实现诸如登录、创建任务、创建任务列表、更新操作、获取任务列表等功能
// 内部通过HttpClient等相关类来发送HTTP请求与服务端通信并且处理了诸如网络异常、JSON解析异常等多种情况同时还管理登录状态、客户端版本等信息。
public class GTaskClient {
// 用于在日志输出中标识该类的标签,取类的简单名称,方便在日志里区分该类相关的记录
private static final String TAG = GTaskClient.class.getSimpleName();
// Google Tasks服务的基础URL用于后续构建具体的请求URL
private static final String GTASK_URL = "https://mail.google.com/tasks/";
// 用于获取Google Tasks数据的URL通常用于获取任务相关信息等操作
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
// 用于向Google Tasks服务提交数据如创建、更新等操作的URL
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// 单例模式下的唯一实例对象初始化为null通过静态方法getInstance获取实例确保整个应用中只有一个GTaskClient实例在运行。
private static GTaskClient mInstance = null;
// 用于发送HTTP请求的HttpClient对象负责实际的网络通信初始化为null在需要进行网络请求时进行初始化和相关配置。
private DefaultHttpClient mHttpClient;
// 用于获取数据的具体URL初始化为GTASK_GET_URL可根据不同情况如登录自定义域名等进行修改指向实际获取数据的地址。
private String mGetUrl;
// 用于提交数据的具体URL初始化为GTASK_POST_URL可根据情况调整指向实际提交数据的地址。
private String mPostUrl;
// 记录客户端的版本号,初始化为 -1在登录成功等操作后从服务端获取并更新用于与服务端交互时标识客户端版本情况。
private long mClientVersion;
// 标记当前是否已登录到Google Tasks服务初始化为false在登录成功后设置为true后续操作根据此标记判断是否需要先登录。
private boolean mLoggedin;
// 记录上次登录的时间戳初始化为0用于判断是否需要重新登录例如根据设定的时间间隔判断单位通常为毫秒。
private long mLastLoginTime;
// 用于为每个操作生成唯一的动作标识符每次获取后自增1确保不同操作有不同的标识方便服务端区分和处理。
private int mActionId;
// 存储当前登录的账户信息Account类型代表与Google Tasks服务交互所使用的账号在登录过程中进行赋值。
private Account mAccount;
// 用于暂存需要更新的节点Node类型相关操作的JSON数组比如任务、任务列表的更新操作等批量处理更新时使用初始化为null。
private JSONArray mUpdateArray;
// 私有构造函数用于初始化GTaskClient对象的各个成员变量按照默认值进行初始化保证单例模式下实例的初始化状态统一。
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
mClientVersion = -1;
mLoggedin = false;
mLastLoginTime = 0;
mActionId = 1;
mAccount = null;
mUpdateArray = null;
}
// 静态方法采用双重检查锁定Double-Checked Locking的方式实现单例模式确保多线程环境下能正确获取唯一的GTaskClient实例。
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
// 登录到Google Tasks服务的方法根据一定的条件判断是否需要重新登录如时间间隔超过设定值、账号切换等
// 如果已登录则直接返回true否则尝试进行登录操作包括获取Google账户认证令牌、根据账户域名情况尝试不同的登录URL等步骤登录成功返回true失败返回false。
public boolean login(Activity activity) {
// 假设Cookie在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, "already logged in");
return true;
}
mLastLoginTime = System.currentTimeMillis();
// 尝试获取Google账户的认证令牌用于后续登录Google Tasks服务如果获取失败则返回false
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// 如果账户名不是以"gmail.com"或"googlemail.com"结尾说明可能是自定义域名账户需要构建自定义域名的登录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";
// 使用自定义域名的URL尝试登录Google Tasks服务如果成功则标记为已登录
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;
}
// 从Android系统账户管理器中获取Google账户的认证令牌的方法首先获取所有的Google类型账户
// 然后根据设置中指定的同步账户名称找到对应的账户再通过账户管理器获取认证令牌如果获取过程出现异常则返回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;
}
// 获取认证令牌通过账户管理器发起获取令牌的异步请求并等待结果如果获取失败则记录日志并返回null
AccountManagerFuture<Bundle> 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 Tasks服务的方法如果登录失败可能是令牌过期会尝试先使令牌失效再重新获取并登录
// 只有两次尝试都失败才返回false表示登录Google Tasks服务最终失败。
private boolean tryToLoginGtask(Activity activity, String authToken) {
if (!loginGtask(authToken)) {
// 可能认证令牌已过期,先使令牌失效,然后重新获取令牌并再次尝试登录
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 Tasks服务的方法配置HttpClient的连接超时、读取超时等参数设置Cookie存储等
// 发送HTTP GET请求到登录URL获取登录后的Cookie和客户端版本号等信息如果过程中出现JSON解析异常、网络请求异常等情况则返回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);
// 构建登录的URL带上认证令牌参数
try {
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// 获取登录后的Cookie信息检查是否包含特定的认证Cookie以"GTL"命名的Cookie
List<Cookie> 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");
}
// 从登录响应内容中提取客户端版本号信息通过解析特定格式的JSON字符串来获取版本号如果JSON解析出现异常则返回false
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
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) {
// 简单地捕获所有其他异常情况记录日志并返回false表示登录失败
Log.e(TAG, "httpget gtask_url failed");
return false;
}
return true;
}
// 获取下一个操作的唯一标识符的方法每次调用该方法会使内部的操作标识符mActionId自增1并返回当前的标识符值用于区分不同的操作请求。
private int getActionId() {
return mActionId++;
}
// 创建一个用于HTTP POST请求的HttpPost对象的方法设置请求头的Content-Type为"application/x-www-form-urlencoded;charset=utf-8"以及其他自定义的请求头信息(如"AT"设为"1"返回配置好的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实体通常是HTTP响应的内容实体中获取响应内容的方法根据内容的编码格式如gzip、deflate等进行相应的解压处理
// 然后逐行读取内容并拼接成字符串返回如果出现IO异常则在方法执行完毕后关闭输入流并向上抛出异常。
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();
}
}
// 向Google Tasks服务发送POST请求的方法首先检查是否已登录未登录则抛出异常然后构建包含请求数据的HttpPost对象
// 设置请求实体将JSON数据转换为URL编码的表单实体发送请求并获取响应将响应内容解析为JSONObject后返回
// 如果在请求过程中出现客户端协议异常、IO异常、JSON解析异常等各种情况则分别抛出对应的异常NetworkFailureException或ActionFailureException
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<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// 执行POST请求获取响应对象并从响应中获取内容解析为JSONObject返回
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 ActionFailureException("get task lists: handing jasonobject failed");
}
}
// 用于获取指定任务列表通过列表的唯一标识符listGid来指定中所有任务信息的方法以JSONArray形式返回任务列表对应的任务数据。
// 该方法会先提交之前暂存的更新操作通过调用commitUpdate方法然后构建请求数据发送到服务器解析响应获取任务信息若过程出现异常则向外抛出相应异常。
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
// 先提交之前暂存的更新操作,确保获取任务列表时之前的更新已经处理或者发送到服务器端,避免数据不一致等问题。
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// action_list
// 设置操作类型为获取全部GETALL对应的值从GTaskStringUtils中获取表明此次请求是要获取指定任务列表下的所有任务信息。
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL);
// 为此次操作生成一个唯一的动作标识符通过调用getActionId方法获取用于服务器端区分不同的操作请求。
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
// 设置要获取任务的任务列表的唯一标识符将传入的listGid参数放入对应的JSON字段中告诉服务器要获取哪个任务列表下的任务。
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
// 设置不获取已删除的任务将对应的JSON字段设置为false只获取当前未删除状态的任务信息。
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
// 将包含此次操作相关信息的action对象添加到actionList数组中后续会将actionList作为请求数据的一部分发送给服务器。
actionList.put(action);
// 将actionList数组放入外层的jsPost对象中对应键为GTaskStringUtils.GTASK_JSON_ACTION_LIST构建出符合服务器要求格式的请求数据结构。
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
// 将客户端当前的版本号放入请求数据中对应键为GTaskStringUtils.GTASK_JSON_CLIENT_VERSION服务器端可根据此版本号来判断兼容性等情况进行相应处理。
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求将构建好的jsPost请求数据发送到服务器并获取服务器返回的响应数据解析为JSONObject对象。
JSONObject jsResponse = postRequest(jsPost);
// 从服务器返回的响应数据jsResponse中提取任务列表对应的任务信息以JSONArray形式返回对应的键为GTaskStringUtils.GTASK_JSON_TASKS这里假设服务器按约定格式返回了任务数据。
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
// 如果在构建请求数据、解析服务器响应数据等过程中出现JSON解析异常记录错误日志打印异常堆栈信息并抛出表示获取任务列表操作中处理JSON对象失败的异常。
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task list: handing jsonobject failed");
}
}
// 用于获取当前用于同步操作的账户信息Account类型的方法直接返回成员变量mAccount该变量在登录等相关操作中被赋值代表与Google Tasks服务交互所使用的账号。
public Account getSyncAccount() {
return mAccount;
}
// 用于重置更新操作数组mUpdateArray的方法将mUpdateArray设置为null通常用于清空之前暂存的更新操作相关数据例如在某些特定场景下重新开始积累更新操作时使用。
public void resetUpdateArray() {
mUpdateArray = null;
}