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

849 lines
34 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;
/**
* GTask客户端核心类采用**单例模式**实现负责与Google TasksGTask服务进行底层网络交互
* 该类是GTask同步功能的核心通信层主要完成以下职责
* 1. Google账户认证获取Google账户的AuthToken处理账户切换、Token失效重取逻辑
* 2. GTask服务登录通过AuthToken登录GTask服务获取认证Cookie和客户端版本号client_version
* 3. HTTP请求处理封装GET/POST请求处理Gzip/Deflate压缩的响应数据解析JSON结果
* 4. GTask核心操作实现任务Task/任务列表TaskList的创建、更新、删除、移动以及列表数据的获取
* 5. 批量更新优化维护更新动作数组限制单次批量更新的最大数量10条减少网络请求次数。
*
* 注意该类依赖Apache HttpClient已被Android高版本弃用但保留原有逻辑
* 所有网络操作会抛出{@link NetworkFailureException}(网络异常)或{@link ActionFailureException}(业务逻辑异常)。
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class GTaskClient {
/**
* 日志标签,使用类的简单名称,便于调试时定位日志来源
*/
private static final String TAG = GTaskClient.class.getSimpleName();
// ====================== GTask服务URL常量 ======================
/**
* GTask服务基础URL
*/
private static final String GTASK_URL = "https://mail.google.com/tasks/";
/**
* GTask服务GET请求URL用于登录、获取任务列表元数据
*/
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
/**
* GTask服务POST请求URL用于执行创建、更新、移动、删除等操作
*/
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// ====================== 单例模式相关 ======================
/**
* GTaskClient单例实例通过{@link #getInstance()}获取
*/
private static GTaskClient mInstance = null;
// ====================== 网络请求相关成员变量 ======================
/**
* Apache HttpClient实例用于发送HTTP请求、管理Cookie
*/
private DefaultHttpClient mHttpClient;
/**
* 动态的GTask GET请求URL适配自定义域名账户如企业邮箱
*/
private String mGetUrl;
/**
* 动态的GTask POST请求URL适配自定义域名账户
*/
private String mPostUrl;
// ====================== GTask服务认证/版本相关 ======================
/**
* GTask客户端版本号从GTask服务返回的JSON中解析用于请求标识
*/
private long mClientVersion;
/**
* 登录状态标记true表示已成功登录GTask服务false表示未登录/需要重新登录
*/
private boolean mLoggedin;
/**
* 最后一次登录时间戳用于判断是否需要重新登录默认5分钟有效期
*/
private long mLastLoginTime;
/**
* 动作ID自增的唯一标识用于标记每个GTask操作的动作创建、更新等
*/
private int mActionId;
/**
* 当前同步的Google账户实例存储账户名称、类型等信息
*/
private Account mAccount;
// ====================== 批量更新相关 ======================
/**
* 更新动作数组存储待提交的更新动作Task/TaskList的updateAction用于批量提交
*/
private JSONArray mUpdateArray;
/**
* 私有构造方法初始化GTaskClient的默认属性单例模式禁止外部实例化
* 初始化URL、状态标记、计数器等为默认值确保单例的唯一性
*/
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; // 初始无批量更新动作
}
/**
* 获取GTaskClient的单例实例线程安全的同步方法
* @return GTaskClient唯一实例
*/
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
/**
* 执行GTask服务的登录流程核心登录方法
* 登录逻辑分为三步:
* 1. 检查登录状态若5分钟内已登录且账户未切换直接返回成功
* 2. 获取Google账户的AuthToken通过AccountManager获取当前同步账户的认证Token
* 3. 登录GTask服务先尝试自定义域名URL非Gmail账户失败则使用官方URL
* @param activity 上下文Activity用于AccountManager获取Token、处理账户授权
* @return true表示登录成功false表示登录失败
*/
public boolean login(Activity activity) {
// 步骤1判断登录有效期5分钟超时则标记为未登录
final long interval = 1000 * 60 * 5; // 5分钟毫秒数
if (mLastLoginTime + interval < System.currentTimeMillis()) {
mLoggedin = false;
}
// 步骤2判断账户是否切换切换则标记为未登录
if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity.getSyncAccountName(activity))) {
mLoggedin = false;
}
// 步骤3已登录则直接返回成功
if (mLoggedin) {
Log.d(TAG, "already logged in");
return true;
}
// 步骤4记录本次登录时间开始新的登录流程
mLastLoginTime = System.currentTimeMillis();
// 获取Google账户的AuthToken
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// 步骤5处理非Gmail/GoogleMail账户自定义域名如企业邮箱
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase().endsWith("googlemail.com"))) {
// 构建自定义域名的GTask URL
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
// 尝试使用自定义URL登录GTask
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// 步骤6自定义URL登录失败/是Gmail账户使用官方URL登录
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
// 登录成功,标记状态
mLoggedin = true;
return true;
}
/**
* 登录Google账户并获取AuthToken底层账户认证方法
* 流程:
* 1. 获取设备上的所有Google账户
* 2. 匹配设置中的同步账户名称;
* 3. 通过AccountManager获取该账户的AuthToken类型为goanna_mobile
* 4. 若传入invalidateToken为true失效旧Token并重新获取
* @param activity 上下文Activity
* @param invalidateToken 是否失效旧的AuthToken用于Token过期时重取
* @return Google账户的AuthToken获取失败则返回null
*/
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
// 获取AccountManager服务
AccountManager accountManager = AccountManager.get(activity);
// 获取所有Google类型的账户type为com.google
Account[] accounts = accountManager.getAccountsByType("com.google");
// 无Google账户返回null
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;
}
// 获取AuthToken
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthToken(
account,
"goanna_mobile", // GTask服务的Token类型
null,
activity,
null,
null
);
try {
// 获取Token结果
Bundle authTokenBundle = accountManagerFuture.getResult();
authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
// 失效旧Token并重新获取递归调用
if (invalidateToken) {
accountManager.invalidateAuthToken("com.google", authToken);
authToken = loginGoogleAccount(activity, false);
}
} catch (Exception e) {
Log.e(TAG, "get auth token failed");
authToken = null;
}
return authToken;
}
/**
* 尝试登录GTask服务处理Token失效重取逻辑
* 流程:
* 1. 使用传入的AuthToken登录GTask
* 2. 若登录失败失效旧Token并重新获取再次尝试登录
* 3. 两次失败则返回false否则返回true
* @param activity 上下文Activity
* @param authToken Google账户的AuthToken
* @return true表示登录成功false表示登录失败
*/
private boolean tryToLoginGtask(Activity activity, String authToken) {
if (!loginGtask(authToken)) {
// Token过期失效并重新获取
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;
}
/**
* 实际执行GTask服务的登录核心网络登录方法
* 流程:
* 1. 初始化HttpClient设置连接超时、Socket超时配置CookieStore
* 2. 发送GET请求携带AuthToken访问GTask的GET URL获取响应
* 3. 检查认证Cookie判断响应中是否包含GTL认证Cookie
* 4. 解析客户端版本号从响应中提取_setup()方法内的JSON获取client_version
* @param authToken Google账户的AuthToken
* @return true表示登录成功false表示登录失败
*/
private boolean loginGtask(String authToken) {
// 配置HTTP参数连接超时10秒Socket超时15秒
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
// 初始化HttpClient配置CookieStore
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore);
// 禁用Expect-Continue头避免部分服务器不兼容
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// 执行GTask登录
try {
// 构建登录URL携带AuthToken
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = mHttpClient.execute(httpGet);
// 检查认证CookieGTL开头的Cookie为GTask认证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");
}
// 解析响应内容获取客户端版本号client_version
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;
// 截取_setup()方法内的JSON字符串
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
// 解析JSON获取client_version
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
} catch (Exception e) {
// 捕获所有异常HTTP请求、IO、解析等
Log.e(TAG, "httpget gtask_url failed");
return false;
}
return true;
}
/**
* 获取自增的动作ID每次调用后ID+1
* 每个GTask操作创建、更新、移动等需要唯一的动作ID标识
* @return 下一个动作ID
*/
private int getActionId() {
return mActionId++;
}
/**
* 创建HTTP POST请求封装POST请求的公共配置
* 设置请求头Content-Type为form-urlencodedAT为1GTask服务要求
* @return 配置好的HttpPost实例
*/
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
// 设置内容类型表单编码UTF-8字符集
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// GTask服务要求的AT头固定为1
httpPost.setHeader("AT", "1");
return httpPost;
}
/**
* 解析HTTP响应的内容处理Gzip/Deflate压缩
* 流程:
* 1. 获取响应的编码类型Content-Encoding
* 2. 根据编码类型创建对应的输入流GzipInputStream/InflaterInputStream
* 3. 读取输入流内容,转换为字符串返回;
* @param entity HTTP响应的实体HttpEntity
* @return 响应的字符串内容
* @throws IOException IO异常流读取失败、关闭失败等
*/
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
// 获取响应的编码类型gzip/deflate/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压缩使用GZIPInputStream解压缩
input = new GZIPInputStream(entity.getContent());
} else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
// Deflate压缩使用InflaterInputStream解压缩启用nowrap模式
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请求到GTask服务核心网络请求方法
* 流程:
* 1. 检查登录状态:未登录则抛出异常;
* 2. 构建POST请求将JSON参数封装为表单参数key为r
* 3. 执行POST请求获取响应并解析为JSON对象返回
* 4. 异常处理:捕获不同异常,抛出对应的自定义异常;
* @param js 要发送的JSON参数对象
* @return GTask服务返回的JSON响应对象
* @throws NetworkFailureException 网络异常客户端协议错误、IO错误
* @throws ActionFailureException 业务逻辑异常未登录、JSON解析失败
*/
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 {
// 封装JSON参数为表单参数key为r值为JSON字符串
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// 执行POST请求
HttpResponse response = mHttpClient.execute(httpPost);
// 解析响应内容为JSON对象
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");
}
}
/**
* 创建任务Task到GTask服务
* 流程:
* 1. 提交已有的批量更新动作(确保之前的更新生效);
* 2. 构建创建任务的JSON请求包含动作列表、客户端版本
* 3. 发送POST请求获取响应中的新任务GID并设置到Task对象
* @param task 要创建的Task对象
* @throws NetworkFailureException 网络异常
* @throws ActionFailureException JSON解析/业务逻辑异常
*/
public void createTask(Task task) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建任务的动作getCreateAction返回创建动作的JSON
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);
// 解析响应中的新任务GIDNEW_ID字段
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");
}
}
/**
* 创建任务列表TaskList到GTask服务
* 流程与创建任务一致区别在于使用TaskList的创建动作
* @param tasklist 要创建的TaskList对象
* @throws NetworkFailureException 网络异常
* @throws 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);
// 发送POST请求
JSONObject jsResponse = postRequest(jsPost);
// 解析响应中的新列表GID
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中的更新动作发送到GTask服务
* 若更新数组不为空则构建POST请求发送发送后清空数组
* @throws NetworkFailureException 网络异常
* @throws 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);
// 发送POST请求
postRequest(jsPost);
// 清空更新数组
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed");
}
}
}
/**
* 添加更新动作到批量更新数组(优化网络请求,批量提交)
* 逻辑:
* 1. 若更新数组大小超过10先提交已有动作避免单次请求数据过大
* 2. 若数组为空,初始化数组;
* 3. 将节点的更新动作添加到数组;
* @param node 要更新的节点Task/TaskList
* @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();
}
// 添加节点的更新动作
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
/**
* 移动任务(在不同列表间/同列表内移动任务)
* 流程:
* 1. 提交已有批量更新动作;
* 2. 构建移动动作的JSON请求区分同列表/不同列表的参数;
* 3. 发送POST请求执行移动操作
* @param task 要移动的任务
* @param preParent 任务的原父列表
* @param curParent 任务的新父列表
* @throws NetworkFailureException 网络异常
* @throws ActionFailureException JSON解析/业务逻辑异常
*/
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
// 同列表移动且任务非第一个添加前序兄弟ID用于排序
if (preParent == curParent && task.getPriorSibling() != null) {
action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling());
}
// 原列表ID
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
// 新父列表ID
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("move task: handing jsonobject failed");
}
}
/**
* 删除节点Task/TaskList标记为已删除并发送更新请求
* 流程:
* 1. 提交已有批量更新动作;
* 2. 设置节点的deleted标记为true
* 3. 构建删除动作的JSON请求并发送
* 4. 清空更新数组;
* @param node 要删除的节点Task/TaskList
* @throws NetworkFailureException 网络异常
* @throws ActionFailureException JSON解析/业务逻辑异常
*/
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 标记节点为已删除
node.setDeleted(true);
// 添加删除动作更新动作包含deleted标记
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("delete node: handing jsonobject failed");
}
}
/**
* 获取所有任务列表的元数据从GTask服务获取
* 流程:
* 1. 检查登录状态;
* 2. 发送GET请求到GTask的GET URL
* 3. 解析响应中的任务列表JSON数组lists字段
* @return 任务列表的JSON数组
* @throws NetworkFailureException 网络异常
* @throws ActionFailureException JSON解析/业务逻辑异常
*/
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 = 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);
// 返回lists字段的JSON数组
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");
}
}
/**
* 获取指定任务列表下的所有任务从GTask服务获取
* 流程:
* 1. 提交已有批量更新动作;
* 2. 构建GETALL动作的JSON请求指定列表ID不获取已删除任务
* 3. 发送POST请求返回任务数组
* @param listGid 任务列表的GID
* @return 任务的JSON数组
* @throws NetworkFailureException 网络异常
* @throws ActionFailureException JSON解析/业务逻辑异常
*/
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// 配置GETALL动作获取列表下的所有任务
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); // 指定列表ID
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);
// 解析响应中的任务数组
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");
}
}
/**
* 获取当前同步的Google账户
* @return 当前的Account实例未登录则返回null
*/
public Account getSyncAccount() {
return mAccount;
}
/**
* 重置批量更新数组(清空待提交的更新动作)
* 用于同步取消、异常处理时清空未提交的更新
*/
public void resetUpdateArray() {
mUpdateArray = null;
}
}