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.
read/src/net/micode/notes/data/GClient.java

651 lines
27 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.

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(); // 日志标签
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"; // 获取数据的URL
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; // 提交数据的URL
private static GTaskClient mInstance = null; // 单例实例
private DefaultHttpClient mHttpClient; // HTTP客户端
private String mGetUrl; // 获取数据的具体URL
private String mPostUrl; // 提交数据的具体URL
private long mClientVersion; // 客户端版本号
private boolean mLoggedin; // 是否已登录
private long mLastLoginTime; // 上次登录时间
private int mActionId; // 操作ID
private Account mAccount; // 当前使用的账户
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;
}
/**
* 获取GTaskClient的单例实例。
* @return GTaskClient的单例实例
*/
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
/**
* 登录Google账户如果需要则重新登录。
* @param activity 当前的Activity实例
* @return 如果登录成功则返回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;
}
// 如果已经登录直接返回true
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;
}
// 如果账户不在gmail.com或googlemail.com域中则尝试使用自定义域名登录
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase()
.endsWith("googlemail.com"))) {
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix + "/");
mGetUrl = url.toString() + "ig";
mPostUrl = url.toString() + "r/ig";
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// 尝试使用Google官方URL登录
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
mLoggedin = true; // 登录成功
return true;
}
/**
* 登录Google账户获取认证令牌。
* @param activity 当前的Activity实例
* @param invalidateToken 是否使当前令牌失效
* @return 认证令牌字符串
*/
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
AccountManager accountManager = AccountManager.get(activity); // 获取AccountManager实例
Account[] accounts = accountManager.getAccountsByType("com.google"); // 获取所有Google账户
if (accounts.length == 0) { // 没有可用的Google账户
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;
}
// 获取认证令牌
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 认证令牌
* @return 如果登录成功则返回true否则返回false
*/
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 认证令牌
* @return 如果登录成功则返回true否则返回false
*/
private boolean loginGtask(String authToken) {
int timeoutConnection = 10000; // 连接超时时间
int timeoutSocket = 15000; // 套接字超时时间
HttpParams httpParameters = new BasicHttpParams(); // HTTP参数
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); // 设置连接超时时间
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); // 设置套接字超时时间
mHttpClient = new DefaultHttpClient(httpParameters); // 创建HTTP客户端
BasicCookieStore localBasicCookieStore = new BasicCookieStore(); // 创建Cookie存储
mHttpClient.setCookieStore(localBasicCookieStore); // 设置Cookie存储
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); // 设置使用Expect Continue
// 登录Google Tasks
try {
String loginUrl = mGetUrl + "?auth=" + authToken; // 构建登录URL
HttpGet httpGet = new HttpGet(loginUrl); // 创建HttpGet请求
HttpResponse response = mHttpClient.execute(httpGet); // 执行请求
// 获取认证cookie
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) { // 检查是否存在认证cookie
hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
Log.w(TAG, "it seems that there is no auth cookie");
}
// 获取客户端版本号
String resString = getResponseContent(response.getEntity()); // 获取响应内容
String jsBegin = "_setup("; // JSON数据开始标记
String jsEnd = ")}</script>"; // JSON数据结束标记
int begin = resString.indexOf(jsBegin); // 查找开始标记
int end = resString.lastIndexOf(jsEnd); // 查找结束标记
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end); // 提取JSON字符串
}
JSONObject js = new JSONObject(jsString); // 将JSON字符串转换为JSONObject
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++;
}
/**
* 创建一个HttpPost请求。
* @return HttpPost请求对象
*/
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl); // 创建HttpPost请求
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 如果发生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")) { // 如果是gzip编码
input = new GZIPInputStream(entity.getContent()); // 使用GZIPInputStream解码
} else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { // 如果是deflate编码
Inflater inflater = new Inflater(true);
input = new InflaterInputStream(entity.getContent(), inflater); // 使用InflaterInputStream解码
}
try {
InputStreamReader isr = new InputStreamReader(input); // 创建InputStreamReader
BufferedReader br = new BufferedReader(isr); // 创建BufferedReader
StringBuilder sb = new StringBuilder();
// 读取响应内容
String buff;
while ((buff = br.readLine()) != null) {
sb.append(buff); // 将每一行内容追加到StringBuilder中
}
return sb.toString();
} finally {
input.close(); // 关闭输入流
}
}
/**
* 提交一个POST请求到Google Tasks。
* @param js 包含操作的JSONObject
* @return 服务器返回的JSONObject
* @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(); // 创建HttpPost请求
try {
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString())); // 将JSONObject转换为字符串并添加到请求参数中
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); // 创建UrlEncodedFormEntity
httpPost.setEntity(entity); // 设置请求实体
// 执行POST请求
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity()); // 获取响应内容
return new JSONObject(jsString); // 将响应内容转换为JSONObject
} 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"); // 如果发生IO错误抛出异常
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("unable to convert response content to jsonobject"); // 如果无法将响应内容转换为JSONObject抛出异常
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("error occurs when posting request"); // 如果发生其他错误,抛出异常
}
}
/**
* 创建一个新的任务。
* @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);
// 提交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)); // 设置任务ID
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create task: handing jsonobject failed"); // 如果发生JSON处理错误抛出异常
}
}
/**
* 创建一个新的任务列表。
* @param tasklist 要创建的任务列表对象
* @throws NetworkFailureException 如果发生网络错误
*/
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate(); // 提交所有待更新的操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建任务列表的操作
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 设置客户端版本号
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)); // 设置任务列表ID
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create tasklist: handing jsonobject failed"); // 如果发生JSON处理错误抛出异常
}
}
/**
* 提交所有待更新的操作。
* @throws NetworkFailureException 如果发生网络错误
*/
public void commitUpdate() throws NetworkFailureException {
if (mUpdateArray != null) { // 如果有待更新的操作
try {
JSONObject jsPost = new JSONObject();
// 设置操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// 设置客户端版本号
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost); // 提交POST请求
mUpdateArray = null; // 清空待更新的操作列表
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed"); // 如果发生JSON处理错误抛出异常
}
}
}
/**
* 添加一个更新操作到待更新的操作列表中。
* @param node 要更新的节点对象
* @throws NetworkFailureException 如果发生网络错误
*/
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// 如果待更新的操作数量超过10个则先提交这些操作
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
if (mUpdateArray == null) {
mUpdateArray = new JSONArray(); // 如果操作列表为空则创建一个新的JSONArray
}
mUpdateArray.put(node.getUpdateAction(getActionId())); // 将节点的更新操作添加到操作列表中
}
}
/**
* 将一个任务移动到另一个任务列表。
* @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.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) {
// 如果在同一个列表中移动并且不是第一个任务则设置前一个兄弟节点ID
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) {
// 如果移动到不同的列表则设置目标列表ID
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
actionList.put(action); // 将操作添加到操作列表中
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 设置客户端版本号
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost); // 提交POST请求
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("move task: handing jsonobject failed"); // 如果发生JSON处理错误抛出异常
}
}
/**
* 删除一个节点。
* @param node 要删除的节点对象
* @throws NetworkFailureException 如果发生网络错误
*/
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate(); // 提交所有待更新的操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 构建删除节点的操作
node.setDeleted(true);
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 设置客户端版本号
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost); // 提交POST请求
mUpdateArray = null; // 清空待更新的操作列表
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("delete node: handing jsonobject failed"); // 如果发生JSON处理错误抛出异常
}
}
/**
* 获取所有任务列表。
* @return 包含所有任务列表的JSONArray
* @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); // 创建HttpGet请求
HttpResponse response = mHttpClient.execute(httpGet); // 执行请求
// 获取任务列表数据
String resString = getResponseContent(response.getEntity()); // 获取响应内容
String jsBegin = "_setup("; // JSON数据开始标记
String jsEnd = ")}</script>"; // JSON数据结束标记
int begin = resString.indexOf(jsBegin); // 查找开始标记
int end = resString.lastIndexOf(jsEnd); // 查找结束标记
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end); // 提取JSON字符串
}
JSONObject js = new JSONObject(jsString); // 将JSON字符串转换为JSONObject
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"); // 如果发生IO错误抛出异常
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task lists: handing jasonobject failed"); // 如果发生JSON处理错误抛出异常
}
}
/**
* 获取指定ID的任务列表中的所有任务。
* @param listGid 任务列表的ID
* @return 包含指定任务列表中所有任务的JSONArray
* @throws NetworkFailureException 如果发生网络错误
*/
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate(); // 提交所有待更新的操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// 构建获取任务列表的操作
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);
// 设置客户端版本号
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 提交POST请求
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"); // 如果发生JSON处理错误抛出异常
}
}
/**
* 获取当前同步的账户。
* @return 当前同步的账户对象
*/
public Account getSyncAccount() {
return mAccount;
}
/**
* 重置待更新的操作列表。
*/
public void resetUpdateArray() {
mUpdateArray = null;
}
}