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.
MiNote/gtask/remote/GTaskClient.java

430 lines
17 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)
*
* 遵循 Apache 许可证 2.0 版("许可证"
* 除非遵守许可证,否则不得使用此文件。
* 您可以在以下网址获取许可证副本:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 除非适用法律要求或书面同意,否则根据许可证分发的软件是按"原样"提供的,
* 不附带任何明示或暗示的保证或条件。请参阅许可证,了解管理权限和限制的具体语言。
*/
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 Tasks 客户端类
* 负责与 Google Tasks 服务进行网络通信,实现任务和任务列表的增删改查及同步
*/
public class GTaskClient {
private static final String TAG = GTaskClient.class.getSimpleName();
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"; // GET 请求 URL获取数据
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; // POST 请求 URL提交操作
private static GTaskClient mInstance = null; // 单例实例
private DefaultHttpClient mHttpClient; // HTTP 客户端
private String mGetUrl; // 当前 GET 请求 URL支持自定义域名
private String mPostUrl; // 当前 POST 请求 URL支持自定义域名
private long mClientVersion; // Google Tasks 客户端版本号(用于协议兼容)
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; // 操作 ID 从 1 开始
mAccount = null; // 初始无账号
mUpdateArray = null; // 初始无待更新操作
}
/**
* 获取单例实例
* @return GTaskClient 实例
*/
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
/**
* 登录 Google 账号并初始化 Tasks 客户端
* @param activity 调用登录的 Activity用于获取账号令牌
* @return 是否登录成功
*/
public boolean login(Activity activity) {
// 登录状态检查:超过 5 分钟或账号变更则重新登录
final long interval = 1000 * 60 * 5; // 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(); // 更新最后登录时间
String authToken = loginGoogleAccount(activity, false); // 获取 Google 账号令牌
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// 处理自定义域名账号(非 @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"; // 拼接自定义 GET URL
mPostUrl = url.toString() + "r/ig"; // 拼接自定义 POST URL
if (tryToLoginGtask(activity, authToken)) { // 尝试登录 Tasks
mLoggedin = true;
}
}
// 处理官方域名账号(默认情况)
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) { // 再次尝试登录
return false;
}
}
mLoggedin = true; // 登录成功
return true;
}
/**
* 登录 Google 账号获取认证令牌
* @param activity Activity 上下文
* @param invalidateToken 是否失效现有令牌(用于重试场景)
* @return 认证令牌,失败返回 null
*/
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
AccountManager accountManager = AccountManager.get(activity);
Account[] accounts = accountManager.getAccountsByType("com.google"); // 获取所有 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) {
Log.e(TAG, "unable to get an account with the same name in the settings");
return null;
}
mAccount = account; // 保存当前账号
// 获取认证令牌
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 服务
* @param activity Activity 上下文
* @param authToken Google 认证令牌
* @return 是否登录成功
*/
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 服务
* @param authToken Google 认证令牌
* @return 是否登录成功
*/
private boolean loginGtask(String authToken) {
int timeoutConnection = 10000; // 连接超时 10 秒
int timeoutSocket = 15000; // 套接字超时 15 秒
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
mHttpClient = new DefaultHttpClient(httpParameters); // 创建带超时配置的 HTTP 客户端
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore); // 使用内存 Cookie 存储
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
try {
// 构造登录 URL携带认证令牌
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = mHttpClient.execute(httpGet); // 发送 GET 请求
// 检查响应中的认证 Cookie
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) { // GTL Cookie 是 Tasks 认证标识
hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
Log.w(TAG, "it seems that there is no auth cookie");
}
// 解析响应获取客户端版本号
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) {
Log.e(TAG, "httpget gtask_url failed");
return false;
}
return true;
}
/**
* 生成唯一操作 ID自增计数器
* @return 操作 ID
*/
private int getActionId() {
return mActionId++;
}
/**
* 创建 POST 请求对象(通用方法)
* @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();
// 处理 GZIP 压缩响应
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(entity.getContent());
// 处理 DEFLATE 压缩响应
} 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();
String buff;
while ((buff = br.readLine()) != null) {
sb.append(buff);
}
return sb.toString();
} finally {
input.close(); // 确保流关闭
}
}
/**
* 发送 POST 请求并解析 JSON 响应
* @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 {
// 构造 POST 请求参数(将 JSON 放入请求体)
LinkedList<BasicNameValuePair> list = new LinkedList<>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// 执行 POST 请求
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString); // 解析响应为 JSON
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
throw new NetworkFailureException("postRequest failed"); // 协议异常(网络问题)
} catch (IOException e) {
Log.e(TAG, e.toString());
throw new NetworkFailureException("postRequest failed"); // 输入输出异常(网络问题)
} catch (JSONException e) {
Log.e(TAG, e.toString());
throw new ActionFailureException("unable to convert response content to jsonobject"); // JSON 解析失败(数据格式问题)
} catch (Exception e) {
Log.e(TAG, e.toString());
throw new ActionFailureException("error occurs when posting request"); // 其他异常
}
}
/**
* 创建任务(调用 Google Tasks API
* @param task 要创建的任务对象
* @throws NetworkFailureException 网络异常
*/
public void createTask(Task task) throws NetworkFailureException {
commitUpdate(); // 提交之前的批量操作(如果有)
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建任务的操作到请求中
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); // 携带客户端版本号
// 发送请求并解析响应
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); // 保存任务的 Google ID
} catch (JSONException e) {
Log.e(TAG, e.toString());
throw new ActionFailureException("create task: handing jsonobject failed");
}
}
/**
* 创建任务列表(调用 Google Tasks API
* @param tasklist 要创建的任务列表对象
* @throws NetworkFailureException 网络异常
*/
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate(); // 提交之前的批量操作
try {
JSONObject jsPost = new JSONObject