/* * 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 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 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 = ")}"; 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 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