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

611 lines
25 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.

/*
* 版权所有 (c) 2010-2011MiCode 开源社区 (www.micode.net)
* 根据 Apache 许可证 2.0 版本("许可证")授权;
* 除非符合许可证的规定,否则不得使用本文件。
* 您可以从以下网址获取许可证副本:
* http://www.apache.org/licenses/LICENSE-2.0
* 除非适用法律要求或书面同意,本软件按"原样"分发,
* 没有任何明示或暗示的保证或条件。
* 详见许可证中规定的权限和限制。
* 这是一份标准的Apache许可证2.0版本的开源声明)
*/
// 定义Google任务远程同步包路径
package net.micode.notes.gtask.remote;
// 导入Android账号管理相关类
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; // 日志工具
// 导入AndroidX注解
import androidx.annotation.NonNull; // 非空参数注解
// 导入Google任务数据模型
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; // Google任务字符串常量工具
import net.micode.notes.ui.NotesPreferenceActivity; // 设置界面Activity
// 导入Apache HTTP组件已废弃但部分旧系统仍在使用
import org.apache.http.HttpEntity; // HTTP消息实体接口
import org.apache.http.HttpResponse; // HTTP响应对象
import org.apache.http.client.entity.UrlEncodedFormEntity; // URL编码表单实体
import org.apache.http.client.methods.HttpGet; // HTTP GET请求方法
import org.apache.http.client.methods.HttpPost; // HTTP POST请求方法
import org.apache.http.cookie.Cookie; // Cookie信息对象
import org.apache.http.impl.client.BasicCookieStore; // 基础Cookie存储
import org.apache.http.impl.client.DefaultHttpClient; // 默认HTTP客户端已废弃
import org.apache.http.message.BasicNameValuePair; // 键值对参数对象
import org.apache.http.params.BasicHttpParams; // HTTP参数集合
import org.apache.http.params.HttpConnectionParams; // 连接相关参数
import org.apache.http.params.HttpParams; // HTTP参数接口
import org.apache.http.params.HttpProtocolParams; // 协议相关参数
// 导入JSON处理类
import org.json.JSONArray; // JSON数组处理
import org.json.JSONException; // JSON解析异常
import org.json.JSONObject; // JSON对象处理
// 导入IO流相关类
import java.io.BufferedReader; // 缓冲字符输入流
import java.io.IOException; // IO异常
import java.io.InputStream; // 字节输入流
import java.io.InputStreamReader; // 字节流转字符流
import java.util.LinkedList; // 链表实现
import java.util.List; // 列表接口
// 导入压缩流处理
import java.util.zip.GZIPInputStream; // GZIP解压流
import java.util.zip.Inflater; // 解压器
import java.util.zip.InflaterInputStream; // 解压输入流
// Google任务客户端实现类负责与Google Tasks 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/";
// GET请求URL
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
// POST请求URL
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// 单例实例
private static GTaskClient mInstance = null;
// HTTP客户端使用已废弃的Apache HTTP Client
private DefaultHttpClient mHttpClient;
// GET请求实际URL可能包含自定义域名
private String mGetUrl;
// POST请求实际URL可能包含自定义域名
private String mPostUrl;
// 客户端版本号
private long mClientVersion;
// 登录状态标识
private boolean mLoggedin;
// 上次登录时间戳
private long mLastLoginTime;
// 操作ID计数器
private int mActionId;
// 当前账号信息
private Account mAccount;
// 待提交的更新操作集合
private JSONArray mUpdateArray;
// 私有构造方法(单例模式)
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL; // 默认使用官方GET URL
mPostUrl = GTASK_POST_URL; // 默认使用官方POST URL
mClientVersion = -1; // 初始无效版本
mLoggedin = false; // 初始未登录
mLastLoginTime = 0; // 初始时间戳
mActionId = 1; // 操作ID从1开始
mAccount = null; // 初始无账号
mUpdateArray = null; // 初始无待更新操作
}
// 获取单例实例(线程安全)
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient(); // 懒加载初始化
}
return mInstance;
}
// 登录方法
public boolean login(Activity activity) {
// 检查登录是否过期假设cookie 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, "已经登录");
return true;
}
mLastLoginTime = System.currentTimeMillis(); // 更新登录时间
String authToken = loginGoogleAccount(activity, false); // 获取认证令牌
if (authToken == null) {
Log.e(TAG, "Google账号登录失败");
return false;
}
// 处理非gmail.com 域名的企业账号
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") ||
mAccount.name.toLowerCase().endsWith("googlemail.com"))) {
// 构建自定义域名URL
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix).append("/");
mGetUrl = url.toString() + "ig"; // 更新GET URL
mPostUrl = url.toString() + "r/ig"; // 更新POST URL
// 尝试用自定义域名登录
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// 自定义域名登录失败时尝试官方URL
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL; // 恢复默认GET URL
mPostUrl = GTASK_POST_URL; // 恢复默认POST URL
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
mLoggedin = true; // 标记登录成功
return true;
}
// 获取Google账号认证令牌
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
// 获取账号管理器实例
AccountManager accountManager = AccountManager.get(activity);
// 获取所有Google账号
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<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, "获取认证令牌失败");
authToken = null;
}
return authToken;
}
// 尝试登录Google Tasks服务
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) {
// 设置HTTP请求超时连接10秒socket15秒
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
// 初始化HTTP客户端
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore); // 设置cookie存储
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// 执行登录请求
try {
String loginUrl = mGetUrl + "?auth=" + authToken; // 构造登录URL
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = mHttpClient.execute(httpGet);
// 检查认证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, "缺少认证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 true; // 解析失败返回true触发重试
} catch (Exception e) {
Log.e(TAG, "HTTP GET请求失败");
return true; // 网络异常返回true触发重试
}
return false; // 登录成功返回false
}
// 生成递增的操作ID
private int getActionId() {
return mActionId++;
}
// 创建HTTP POST请求对象
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实体获取响应内容支持gzip/deflate压缩
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "内容编码: " + 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请求并返回JSON响应
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "请先登录");
throw new ActionFailureException("未登录");
}
HttpPost httpPost = createHttpPost();
try {
// 构造请求参数
LinkedList<BasicNameValuePair> list = new LinkedList<>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// 执行请求并解析响应
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("POST请求失败");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("响应内容转JSON失败");
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("请求过程中发生错误");
}
}
// 创建任务
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));
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("创建任务时处理JSON失败");
}
}
// 创建任务列表
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);
// 发送请求并处理响应
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("创建任务列表时处理JSON失败");
}
}
// 提交所有待更新操作
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); // 发送请求
mUpdateArray = null; // 清空待更新操作
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("提交更新时处理JSON失败");
}
}
}
// 添加节点更新操作
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// 避免过多更新操作限制最多10个
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate(); // 超过限制先提交
}
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
mUpdateArray.put(node.getUpdateAction(getActionId())); // 添加更新动作
}
}
// 移动任务
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());
// 同列表移动时设置前驱任务ID
if (preParent == curParent && task.getPriorSibling() != null) {
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());
// 跨列表移动时设置目标列表ID
if (preParent != curParent) {
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); // 发送请求
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("移动任务时处理JSON失败");
}
}
// 删除节点
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); // 发送请求
mUpdateArray = null; // 清空待更新操作
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("删除节点时处理JSON失败");
}
}
// 获取所有任务列表
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "请先登录");
throw new ActionFailureException("未登录");
}
try {
HttpGet httpGet = new HttpGet(mGetUrl); // 创建GET请求
HttpResponse response = mHttpClient.execute(httpGet);
// 从响应中提取任务列表数据
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);
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("获取任务列表时HTTP请求失败");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("获取任务列表时处理JSON失败");
}
}
// 获取特定任务列表详情
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate(); // 先提交待更新操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = getJsonArray(listGid); // 获取动作数组
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本
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("获取任务列表详情时处理JSON失败");
}
}
// 构造获取任务列表的动作数组
@NonNull
private JSONArray getJsonArray(String listGid) throws JSONException {
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);
return actionList;
}
// 获取当前同步账号
public Account getSyncAccount() {
return mAccount;
}
// 重置待更新操作数组
public void resetUpdateArray() {
mUpdateArray = null;
}
}