diff --git a/app/build.gradle b/app/build.gradle index 556ea79e3..6c8693823 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,8 +39,8 @@ repositories { // Version number def versionMajor = 5 // Major UI overhauls def versionMinor = 7 // Some new functionality -def versionPatch = 0 // Bug fixes -def versionBuild = 1 // Bump for dogfood builds, public betas, etc. +def versionPatch = 1 // Bug fixes +def versionBuild = 2 // Bump for dogfood builds, public betas, etc. // Version name from git def getVersionName = { -> @@ -144,6 +144,8 @@ dependencies { compile 'com.nononsenseapps:filepicker:1.2.0' // OrgParser compile 'org.cowboyprogrammer:orgparser:1.1' + // For Sync + compile 'com.squareup.retrofit:retrofit:1.6.1' // Included libraries compile project(':external:ActionBar-PullToRefresh') compile project(':external:datetimepicker') diff --git a/app/src/main/java/com/nononsenseapps/build/Config.java b/app/src/main/java/com/nononsenseapps/build/Config.java index ec9dc4e7e..ce2f950c4 100644 --- a/app/src/main/java/com/nononsenseapps/build/Config.java +++ b/app/src/main/java/com/nononsenseapps/build/Config.java @@ -56,7 +56,7 @@ public static Properties getProperties(final Context context) { public static String getGtasksApiKey(final Context context) { return getProperties(context).getProperty(KEY_GTASKS_API_KEY, - "AIzaSyCAjRk2GfPARlIU3JsaEiExLMtj_rdN2i4"); + "AIzaSyBtUvSWg41WVi9E3W1VaqDMlJ07a3B6JOs"); } public static String getKeyDropboxAPI(final Context context) { diff --git a/app/src/main/java/com/nononsenseapps/helpers/SyncHelper.java b/app/src/main/java/com/nononsenseapps/helpers/SyncHelper.java index cb8827168..37029b2ba 100644 --- a/app/src/main/java/com/nononsenseapps/helpers/SyncHelper.java +++ b/app/src/main/java/com/nononsenseapps/helpers/SyncHelper.java @@ -75,7 +75,7 @@ public static boolean isGTasksConfigured(final Context context) { final String accountName = prefs.getString(SyncPrefs.KEY_ACCOUNT, ""); final boolean syncEnabled = prefs.getBoolean(SyncPrefs.KEY_SYNC_ENABLE, false); - return syncEnabled & accountName != null & !accountName.equals(""); + return syncEnabled && !accountName.isEmpty(); } private static void requestGTaskSyncNow(final Context context) { @@ -88,7 +88,7 @@ private static void requestGTaskSyncNow(final Context context) { final String accountName = prefs.getString(SyncPrefs.KEY_ACCOUNT, ""); - if (accountName != null && !"".equals(accountName)) { + if (!accountName.isEmpty()) { Account account = SyncPrefs.getAccount(AccountManager.get(context), accountName); // Don't start a new sync if one is already going @@ -98,6 +98,7 @@ private static void requestGTaskSyncNow(final Context context) { // in accounts manager. Only use it here where the user has // manually desired a sync to happen NOW. options.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + options.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver .requestSync(account, MyContentProvider.AUTHORITY, options); // Set last sync time to now diff --git a/app/src/main/java/com/nononsenseapps/notepad/prefs/AccountDialog4.java b/app/src/main/java/com/nononsenseapps/notepad/prefs/AccountDialog4.java index 239a1e750..72e2ccde4 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/prefs/AccountDialog4.java +++ b/app/src/main/java/com/nononsenseapps/notepad/prefs/AccountDialog4.java @@ -22,7 +22,7 @@ import com.nononsenseapps.helpers.SyncHelper; import com.nononsenseapps.notepad.R; import com.nononsenseapps.notepad.database.MyContentProvider; -import com.nononsenseapps.notepad.sync.googleapi.GoogleTaskSync; +import com.nononsenseapps.notepad.sync.googleapi.GoogleTasksClient; /** * A copy of AccountDialog in SyncPrefs, but extending from support library @@ -69,13 +69,12 @@ public void onClick(DialogInterface dialog, int which) { * * @param account */ - public void accountSelected(Account account) { + public void accountSelected(final Account account) { if (account != null) { - Log.d("prefsActivity", "step one"); + Log.d("prefsActivityDialog", "step one"); this.account = account; // Request user's permission - AccountManager.get(activity).getAuthToken(account, - GoogleTaskSync.AUTH_TOKEN_TYPE, null, activity, this, null); + GoogleTasksClient.getAuthTokenAsync(activity, account, this); // work continues in callback, method run() } } @@ -87,7 +86,7 @@ public void accountSelected(Account account) { @Override public void run(AccountManagerFuture future) { try { - Log.d("prefsActivity", "step two"); + Log.d("prefsActivityDialog", "step two"); // If the user has authorized // your application to use the // tasks API @@ -95,9 +94,10 @@ public void run(AccountManagerFuture future) { String token = future.getResult().getString( AccountManager.KEY_AUTHTOKEN); // Now we are authorized by the user. + Log.d("prefsActivityDialog", "step two-b: " + token); if (token != null && !token.equals("") && account != null) { - Log.d("prefsActivity", "step three: " + account.name); + Log.d("prefsActivityDialog", "step three: " + account.name); SharedPreferences customSharedPreference = PreferenceManager .getDefaultSharedPreferences(activity); customSharedPreference.edit() diff --git a/app/src/main/java/com/nononsenseapps/notepad/prefs/SyncPrefs.java b/app/src/main/java/com/nononsenseapps/notepad/prefs/SyncPrefs.java index 9e9f6a89d..8352eb3b6 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/prefs/SyncPrefs.java +++ b/app/src/main/java/com/nononsenseapps/notepad/prefs/SyncPrefs.java @@ -22,6 +22,7 @@ import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -48,7 +49,7 @@ import com.nononsenseapps.notepad.BuildConfig; import com.nononsenseapps.notepad.R; import com.nononsenseapps.notepad.database.MyContentProvider; -import com.nononsenseapps.notepad.sync.googleapi.GoogleTaskSync; +import com.nononsenseapps.notepad.sync.googleapi.GoogleTasksClient; import com.nononsenseapps.notepad.sync.orgsync.DropboxSyncHelper; import com.nononsenseapps.notepad.sync.orgsync.DropboxSynchronizer; import com.nononsenseapps.notepad.sync.orgsync.OrgSyncService; @@ -441,14 +442,12 @@ public void onClick(DialogInterface dialog, int which) { * * @param account */ - public void accountSelected(Account account) { + public void accountSelected(final Account account) { if (account != null) { Log.d("prefsActivity", "step one"); this.account = account; // Request user's permission - AccountManager.get(activity).getAuthToken(account, - GoogleTaskSync.AUTH_TOKEN_TYPE, null, activity, this, - null); + GoogleTasksClient.getAuthTokenAsync(activity, account, this); // work continues in callback, method run() } } @@ -457,6 +456,7 @@ public void accountSelected(Account account) { * User wants to select an account to sync with. If we get an approval, * activate sync and set periodicity also. */ + @SuppressLint("CommitPrefEdits") @Override public void run(AccountManagerFuture future) { try { @@ -468,8 +468,9 @@ public void run(AccountManagerFuture future) { String token = future.getResult().getString( AccountManager.KEY_AUTHTOKEN); // Now we are authorized by the user. + Log.d("prefsActivity", "step two-b: " + token); - if (token != null && !token.equals("") && account != null) { + if (token != null && !token.isEmpty() && account != null) { Log.d("prefsActivity", "step three: " + account.name); SharedPreferences customSharedPreference = PreferenceManager .getDefaultSharedPreferences(activity); diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/SyncAdapter.java b/app/src/main/java/com/nononsenseapps/notepad/sync/SyncAdapter.java index 3631fbc52..419d89fa1 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/sync/SyncAdapter.java +++ b/app/src/main/java/com/nononsenseapps/notepad/sync/SyncAdapter.java @@ -60,9 +60,6 @@ */ public class SyncAdapter extends AbstractThreadedSyncAdapter { - // public static final String AUTH_TOKEN_TYPE = - // "oauth2:https://www.googleapis.com/auth/tasks"; - public static final String SYNC_STARTED = "com.nononsenseapps.notepad.sync.SYNC_STARTED"; public static final String SYNC_FINISHED = "com.nononsenseapps.notepad.sync.SYNC_FINISHED"; diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleAPITalker.java b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleAPITalker.java deleted file mode 100644 index b75a35a10..000000000 --- a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleAPITalker.java +++ /dev/null @@ -1,943 +0,0 @@ -/* - * Copyright (C) 2012 Jonas Kalderstam - * - * 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 com.nononsenseapps.notepad.sync.googleapi; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.content.Context; -import android.net.http.AndroidHttpClient; - -import com.nononsenseapps.build.Config; -import com.nononsenseapps.helpers.Log; -import com.nononsenseapps.utils.time.RFC3339Date; - -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.StringEntity; -import org.apache.http.protocol.HTTP; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONTokener; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Random; - -/** - * Helper class that sorts out all XML, JSON, HTTP bullshit for other classes. - * Also keeps track of APIKEY and AuthToken - * - * @author Jonas - * - */ -public class GoogleAPITalker { - - private final Context context; - - /** - * - * @param context Need context to load api key from file - */ - public GoogleAPITalker(final Context context) { - this.context = context; - } - - public static class PreconditionException extends Exception { - private static final long serialVersionUID = 7317567246857384353L; - - public PreconditionException() { - } - - public PreconditionException(String detailMessage) { - super(detailMessage); - } - - public PreconditionException(Throwable throwable) { - super(throwable); - } - - public PreconditionException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - } - - public static class NotModifiedException extends Exception { - private static final long serialVersionUID = -6736829980184373286L; - - public NotModifiedException() { - } - - public NotModifiedException(String detailMessage) { - super(detailMessage); - } - - public NotModifiedException(Throwable throwable) { - super(throwable); - } - - public NotModifiedException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - } - - public static Random rand = new Random(); - - private static final String NEXTPAGETOKEN = "nextPageToken"; - - private String AuthUrlEnd() { - return "key=" + Config.getGtasksApiKey(context); - } - - // public static final String AUTH_URL_END = "key=" + APIKEY; - - private static final String BASE_URL = "https://www.googleapis.com/tasks/v1/users/@me/lists"; - - private String AllLists(final String pageToken) { - String result = BASE_URL + "?"; - - if (pageToken != null && !pageToken.isEmpty()) { - result += "pageToken=" + pageToken + "&"; - } - - result += AuthUrlEnd(); - return result; - } - - private String InsertLists() { - return BASE_URL + "?" + AuthUrlEnd(); - } - - // public static final String ALL_LISTS = BASE_URL + "?" + AUTH_URL_END; - - private String AllListsJustEtag() { - return BASE_URL + "?fields=etag&" + AuthUrlEnd(); - } - - // public static final String ALL_LISTS_JUST_ETAG = BASE_URL + - // "?fields=etag&"+ AUTH_URL_END; - - private String ListURL(String id) { - return BASE_URL + "/" + id + "?" + AuthUrlEnd(); - } - - public static final String LISTS = "/lists"; - private static final String BASE_TASK_URL = "https://www.googleapis.com/tasks/v1/lists"; - private static final String TASKS = "/tasks"; // Must be preceeded by - - // only retrieve the fields we will save in the database or use - // https://www.googleapis.com/tasks/v1/lists/MDIwMzMwNjA0MjM5MzQ4MzIzMjU6MDow/tasks?showDeleted=true&showHidden=true&pp=1&key={YOUR_API_KEY} - // updatedMin=2012-02-07T14%3A59%3A05.000Z - private String AllTasksInsert(String listId) { - return BASE_TASK_URL + "/" + listId + TASKS + "?" + AuthUrlEnd(); - } - - private String TaskURL(String taskId, String listId) { - return BASE_TASK_URL + "/" + listId + TASKS + "/" + taskId + "?" - + AuthUrlEnd(); - } - - private String TaskURL_ETAG_ID_UPDATED(final String taskId, - final String listId) { - String url = BASE_TASK_URL + "/" + listId + TASKS + "/" + taskId - + "?fields=id,etag,updated"; - url += ",position,parent&" + AuthUrlEnd(); - return url; - } - - private String TaskMoveURL_ETAG_UPDATED(final String taskId, - final String listId, final String remoteparent, - final String remoteprevious) { - String url = BASE_TASK_URL + "/" + listId + TASKS + "/" + taskId - + "/move?"; - if (remoteparent != null && !remoteparent.isEmpty()) - url += "parent=" + remoteparent + "&"; - if (remoteprevious != null && !remoteprevious.isEmpty()) - url += "previous=" + remoteprevious + "&"; - url += "fields=etag,updated,position,parent&" + AuthUrlEnd(); - return url; - } - - /** - * Set the pageToken to null to get the first page - */ - private String allTasksUpdatedMin(final String listId, - final String timestamp, final String pageToken) { - // items,nextPageToken - String request = BASE_TASK_URL - + "/" - + listId - + TASKS - + "?showDeleted=true&showHidden=true&fields=items%2CnextPageToken&"; - - // Comes into play if user has Many tasks - if (pageToken != null && !pageToken.isEmpty()) { - request += "pageToken=" + pageToken + "&"; - } - - if (timestamp != null && !timestamp.isEmpty()) { - try { - request += "updatedMin=" - + URLEncoder.encode(timestamp, "UTF-8") + "&"; - } - catch (UnsupportedEncodingException e) { - // Is OK. Can request full sync. - Log.d(TAG, "Malformed timestamp: " + e.getLocalizedMessage()); - } - } - - request += AuthUrlEnd(); - return request; - } - - // Tasks URL which inludes deleted tasks: /tasks?showDeleted=true - // Also do showHidden=true? - // Tasks returnerd will have deleted = true or no deleted field at all. Same - // case for hidden. - private static final String TAG = "nononsenseapps GoogleAPITalker"; - - private static final String USERAGENT = "HoloNotes (gzip)"; - - // A URL is alwasy constructed as: BASE_URL + ["/" + LISTID [+ TASKS [+ "/" - // + TASKID]]] + "?" + [POSSIBLE FIELDS + "&"] + AUTH_URL_END - // Where each enclosing parenthesis is optional - - private String authToken; - - private AndroidHttpClient client; - - public String accountName = null; - - private static String getAuthToken(AccountManager accountManager, - Account account, String authTokenType, boolean notifyAuthFailure) { - - Log.d(TAG, "getAuthToken"); - String authToken = ""; - try { - // Might be invalid in the cache - authToken = accountManager.blockingGetAuthToken(account, - authTokenType, notifyAuthFailure); - accountManager.invalidateAuthToken("com.google", authToken); - - authToken = accountManager.blockingGetAuthToken(account, - authTokenType, notifyAuthFailure); - } - catch (OperationCanceledException ignored) { - } - catch (AuthenticatorException ignored) { - } - catch (IOException ignored) { - } - return authToken; - } - - public boolean initialize(AccountManager accountManager, Account account, - String authTokenType, boolean notifyAuthFailure) { - - Log.d(TAG, "initialize"); - accountName = account.name; - // HttpParams params = new BasicHttpParams(); - // params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, - // HttpVersion.HTTP_1_1); - // client = new AndroidHttpClientHttpClient(params); - client = AndroidHttpClient.newInstance(USERAGENT); - - authToken = getAuthToken(accountManager, account, authTokenType, - notifyAuthFailure); - - Log.d(TAG, "authToken: " + authToken); - return authToken != null && !authToken.equals(""); - } - - public void closeClient() { - if (client != null) { - client.close(); - } - } - - /* - * User methods - */ - - // This is not necessary unless I really want the etags of each list - // public ArrayList getAllLists() - // throws ClientProtocolException, JSONException, - // PreconditionException, NotModifiedException, IOException { - // ArrayList list = new ArrayList(); - // - // // Lists will not carry etags, must fetch them individually - // for (GoogleTaskList gimpedList : getListOfLists()) { - // list.add(getList(gimpedList)); - // } - // - // return list; - // } - - /** - * The entries in this does only one net-call, and such the list items do - * not contain e-tags. useful to get an id-list. - * - * E-tag is an amalgam of etags in all pages if user has more than 100 - * lists. - * - * @return - * @throws IOException - * @throws NotModifiedException - * @throws PreconditionException - * @throws ClientProtocolException - * @throws JSONException - */ - public String getListOfLists(ArrayList list) - throws IOException, JSONException { - String eTag = ""; - String pageToken = null; - do { - HttpGet httpget = new HttpGet(AllLists(pageToken)); - httpget.setHeader("Authorization", "OAuth " + authToken); - - // Log.d(TAG, "request: " + AllLists()); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httpget); - - try { - JSONObject jsonResponse = (JSONObject) new JSONTokener( - parseResponse(client.execute(httpget))).nextValue(); - - // Log.d(TAG, jsonResponse.toString()); - if (jsonResponse.isNull(NEXTPAGETOKEN)) { - pageToken = null; - } - else { - pageToken = jsonResponse.getString(NEXTPAGETOKEN); - } - // No lists - if (jsonResponse.isNull("items")) { - break; - } - - eTag += jsonResponse.getString("etag"); - - JSONArray lists = jsonResponse.getJSONArray("items"); - - int size = lists.length(); - int i; - - // Lists will not carry etags, must fetch them individually if - // that - // is desired - for (i = 0; i < size; i++) { - JSONObject jsonList = lists.getJSONObject(i); - //Log.d("nononsenseapps", jsonList.toString(2)); - list.add(new GoogleTaskList(jsonList, accountName)); - } - } - catch (PreconditionException e) { - // // Can not happen in this case since we don't have any etag! - // } catch (NotModifiedException e) { - // // Can not happen in this case since we don't have any etag! - // } - } - } while (pageToken != null); - - return eTag; - } - - /** - * If etag is present, will make a if-none-match request. Expects only id - * and possibly etag to be present in the object - * - * @param gimpedTask - * @return - * @throws IOException - * @throws NotModifiedException - * @throws JSONException - * @throws ClientProtocolException - * @throws PreconditionException - */ - public GoogleTask getTask(GoogleTask gimpedTask, GoogleTaskList list) - throws JSONException, - IOException, PreconditionException { - GoogleTask result; - HttpGet httpget = new HttpGet(TaskURL(gimpedTask.remoteId, - list.remoteId)); - setAuthHeader(httpget); - // setHeaderWeakEtag(httpget, gimpedTask.etag); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httpget); - - // Log.d(TAG, "request: " + TaskURL(gimpedTask.id, list.id)); - - JSONObject jsonResponse = (JSONObject) new JSONTokener( - parseResponse(client.execute(httpget))).nextValue(); - - // Log.d(TAG, jsonResponse.toString()); - result = new GoogleTask(jsonResponse, accountName); - // } catch (PreconditionException e) { - // // Can not happen since we are not doing a PUT/POST - // } - - result.listdbid = list.dbid; - return result; - } - - /** - * Takes a list object because the etag is optional here. If one is present, - * will make a if-none-match request. - * - * @param gimpedList - * @return - * @throws ClientProtocolException - * @throws JSONException - * @throws PreconditionException - * @throws NotModifiedException - * @throws IOException - */ - public GoogleTaskList getList(GoogleTaskList gimpedList) - throws JSONException, - IOException, PreconditionException { - GoogleTaskList result; - HttpGet httpget = new HttpGet(ListURL(gimpedList.remoteId)); - setAuthHeader(httpget); - // setHeaderWeakEtag(httpget, gimpedList.etag); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httpget); - - // Log.d(TAG, "request: " + ListURL(gimpedList.id)); - - JSONObject jsonResponse = (JSONObject) new JSONTokener( - parseResponse(client.execute(httpget))).nextValue(); - - // Log.d(TAG, jsonResponse.toString()); - result = new GoogleTaskList(jsonResponse, accountName); - // } catch (PreconditionException e) { - // // Can not happen since we are not doing a PUT/POST - // } - - return result; - } - - public String getEtag() throws JSONException, - IOException, PreconditionException { - String eTag; - HttpGet httpget = new HttpGet(AllListsJustEtag()); - httpget.setHeader("Authorization", "OAuth " + authToken); - - // Log.d(TAG, "request: " + AllLists()); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httpget); - - JSONObject jsonResponse = (JSONObject) new JSONTokener( - parseResponse(client.execute(httpget))).nextValue(); - - // Log.d(TAG, jsonResponse.toString()); - - eTag = jsonResponse.getString("etag"); - - // } catch (PreconditionException e) { - // // Can not happen in this case since we don't have any etag! - // } catch (NotModifiedException e) { - // // Can not happen in this case since we don't have any etag! - // } - - return eTag; - } - - /** - * Given a time, will fetch all tasks which were modified afterwards - * - * @param list - * @throws IOException - * @throws ClientProtocolException - * @throws JSONException - */ - public ArrayList getModifiedTasks(String lastUpdated, - GoogleTaskList list) throws IOException, JSONException { - ArrayList moddedList = new ArrayList(); - - // If user has many tasks, they will not all be returned in same request - String pageToken = null; - - // Loop while we have a next page to go to. Always fetch the first page - do { - HttpGet httpget = new HttpGet(allTasksUpdatedMin(list.remoteId, - lastUpdated, pageToken)); - setAuthHeader(httpget); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httpget); - - String stringResponse; - try { - stringResponse = parseResponse(client.execute(httpget)); - - JSONObject jsonResponse = new JSONObject(stringResponse); - - // Log.d(MainActivity.TAG, jsonResponse.toString()); - // If we have a next page, get that - if (jsonResponse.isNull(NEXTPAGETOKEN)) { - pageToken = null; - } - else { - pageToken = jsonResponse.getString(NEXTPAGETOKEN); - } - - // No modified tasks - if (jsonResponse.isNull("items")) { - break; - } - - // Will be an array of items - JSONArray items = jsonResponse.getJSONArray("items"); - - int i; - int length = items.length(); - for (i = 0; i < length; i++) { - JSONObject jsonTask = items.getJSONObject(i); -// Log.d(MainActivity.TAG, -// "moddedJSONTask: " + jsonTask.toString()); - final GoogleTask gt = new GoogleTask(jsonTask, accountName); - gt.listdbid = list.dbid; - moddedList.add(gt); - } - } - catch (PreconditionException e) { - // // Can't happen - return null; - // } catch (NotModifiedException e) { - // - // Log.d(TAG, e.getLocalizedMessage()); - } - } while (pageToken != null); - - return moddedList; - } - - /** - * Returns an object if all went well. Returns null if no upload was done. - * Will set only remote id, etag, position and parent fields. - * - * Updates the task in place and also returns it. - * - * @throws PreconditionException - * @throws JSONException - */ - public GoogleTask uploadTask(final GoogleTask task, - final GoogleTaskList pList) throws - IOException, PreconditionException, JSONException { - - if (pList.remoteId == null || pList.remoteId.isEmpty()) { - Log.d(TAG, "Invalid list ID found for uploadTask"); - return null; // Invalid list id - } - - // If we are trying to upload a deleted task which does not exist on - // server, we can ignore it. might happen with conflicts - if (task.isDeleted() && (task.remoteId == null || task.remoteId.isEmpty())) { - Log.d(TAG, "Trying to upload a deleted non-synced note, ignoring: " - + task.title); - return null; - } - - HttpUriRequest httppost; - if (task.remoteId != null && !task.remoteId.isEmpty()) { - if (task.isDeleted()) { - httppost = new HttpPost(TaskURL(task.remoteId, pList.remoteId)); - httppost.setHeader("X-HTTP-Method-Override", "DELETE"); - } - else { - httppost = new HttpPost(TaskURL_ETAG_ID_UPDATED(task.remoteId, - pList.remoteId)); - // apache does not include PATCH requests, but we can force a - // post to be a PATCH request - httppost.setHeader("X-HTTP-Method-Override", "PATCH"); - } - // Always set ETAGS for tasks - // setHeaderStrongEtag(httppost, task.etag); - } - else { - if (task.isDeleted()) { - return task; // Don't sync deleted items which do not exist on - // the server - } - - //Log.d(TAG, "ID IS NULL: " + task.title); - httppost = new HttpPost(AllTasksInsert(pList.remoteId)); - // task.didRemoteInsert = true; // Need this later - } - setAuthHeader(httppost); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httppost); - - // Log.d(TAG, httppost.getRequestLine().toString()); - // for (Header header : httppost.getAllHeaders()) { - // Log.d(TAG, header.getName() + ": " + header.getValue()); - // } - - if (!task.isDeleted()) { - setPostBody(httppost, task); - } - - String stringResponse = parseResponse(client.execute(httppost)); - - // If we deleted the note, we will get an empty response. Return the - // same element back. - if (task.isDeleted()) { - - Log.d(TAG, "deleted and Stringresponse: " + stringResponse); - } - else { - JSONObject jsonResponse = new JSONObject(stringResponse); - - // Log.d(TAG, jsonResponse.toString()); - - // Will return a task, containing id and etag. always update - // fields - task.remoteId = jsonResponse.getString(GoogleTask.ID); - // task.etag = jsonResponse.getString("etag"); - if (jsonResponse.has(GoogleTask.UPDATED)) { - try { - task.updated = RFC3339Date.parseRFC3339Date( - jsonResponse.getString(GoogleTask.UPDATED)) - .getTime(); - } - catch (Exception e) { - task.updated = 0L; - } - } - } - - return task; - } - - /** - * Calls the GTasks API and tries to move a task to its target position. - * - * @param task - * @throws ClientProtocolException - * @throws JSONException - * @throws IOException - * @throws PreconditionException - */ - // private void moveTask(final GoogleTask task, final GoogleTaskList pList) - // throws ClientProtocolException, JSONException, IOException, - // PreconditionException { - // - // if (pList.id == null || pList.id.isEmpty() || task.id == null - // || task.id.isEmpty()) { - // Log.d(TAG + ".move", "Invalid list ID found for uploadTask"); - // return; - // } - // - // HttpUriRequest httppost = new HttpPost(TaskMoveURL_ETAG_UPDATED( - // task.id, pList.id, task.parent, task.remoteprevious)); - // - // setAuthHeader(httppost); - // AndroidHttpClient.modifyRequestToAcceptGzipResponse(httppost); - // - // // No need since this is only done on sucessful updates - // // setHeaderStrongEtag(httppost, task.etag); - // - // Log.d(TAG + ".move", httppost.getRequestLine().toString()); - // for (Header header : httppost.getAllHeaders()) { - // Log.d(TAG + ".move", header.getName() + ": " + header.getValue()); - // } - // - // String stringResponse = parseResponse(client.execute(httppost)); - // - // JSONObject jsonResponse = new JSONObject(stringResponse); - // - // Log.d(TAG + ".move", jsonResponse.toString()); - // - // // Will return a task, containing id and etag. always update - // // fields - // task.etag = jsonResponse.getString("etag"); - // if (jsonResponse.has(GoogleTask.UPDATED)) - // task.updated = jsonResponse.getString(GoogleTask.UPDATED); - // task.moveUploaded = true; - // if (jsonResponse.has(GoogleTask.PARENT)) { - // task.remoteparent = jsonResponse.getString(GoogleTask.PARENT); - // Log.d(TAG + ".move", jsonResponse.getString(GoogleTask.PARENT)); - // } else - // task.remoteparent = null; - // if (jsonResponse.has(GoogleTask.POSITION)) { - // task.position = jsonResponse.getString(GoogleTask.POSITION); - // Log.d(TAG + ".move", jsonResponse.getString(GoogleTask.POSITION)); - // } else - // task.position = null; - // } - - /** - * Returns an object if all went well. Returns null if a conflict was - * detected. If the list has deleted set to 1, will call the server and - * delete the list instead of updating it. - * - * @throws IOException - * @throws PreconditionException - * @throws JSONException - * @throws ClientProtocolException - */ - public GoogleTaskList uploadList(final GoogleTaskList list) - throws IOException, PreconditionException, JSONException { - final HttpUriRequest httppost; - if (list.remoteId != null) { - //Log.d(TAG, "ID is not NULL!! " + ListURL(list.remoteId)); - if (list.isDeleted()) { - httppost = new HttpDelete(ListURL(list.remoteId)); - } - else { - httppost = new HttpPost(ListURL(list.remoteId)); - // apache does not include PATCH requests, but we can force a - // post to be a PATCH request - httppost.setHeader("X-HTTP-Method-Override", "PATCH"); - } - } - else { - httppost = new HttpPost(InsertLists()); - // list.didRemoteInsert = true; // Need this later - } - setAuthHeader(httppost); - AndroidHttpClient.modifyRequestToAcceptGzipResponse(httppost); - - // Log.d(TAG, httppost.getRequestLine().toString()); - // for (Header header : httppost.getAllHeaders()) { - // Log.d(TAG, header.getName() + ": " + header.getValue()); - // } - - if (!list.isDeleted()) { - setPostBody(httppost, list); - } - - String stringResponse = parseResponse(client.execute(httppost)); - - // If we deleted the note, we will get an empty response. Return the - // same element back. - if (list.isDeleted()) { - Log.d(TAG, "deleted and Stringresponse: " + stringResponse); - } - else { - JSONObject jsonResponse = new JSONObject(stringResponse); - - // Log.d(TAG, jsonResponse.toString()); - - // Will return a list, containing id and etag. always update - // fields - // list.etag = jsonResponse.getString("etag"); - list.remoteId = jsonResponse.getString("id"); - list.title = jsonResponse.getString("title"); - try { - list.updated = RFC3339Date.parseRFC3339Date(jsonResponse.getString("updated")).getTime(); - } - catch (Exception e) { - list.updated = 0L; - } - } - - return list; - } - - /* - * Communication methods - */ - - /** - * Sets the authorization header - * - * @param request - * @return - */ - private void setAuthHeader(HttpUriRequest request) { - if (request != null) - request.setHeader("Authorization", "OAuth " + authToken); - } - - /** - * Does nothing if etag is null or "" Sets an if-match header for strong - * etag comparisons. - * - * @param etag - */ - private void setHeaderStrongEtag(final HttpUriRequest httppost, - final String etag) { - if (etag != null && !etag.isEmpty()) { - httppost.setHeader("If-Match", etag); - - //Log.d(TAG, "If-Match: " + etag); - } - else { - //Log.d(TAG, "No ETAG could be found!"); - } - } - - /** - * Does nothing if etag is null or "" Sets an if-none-match header for weak - * etag comparisons. - * - * If-None-Match: W/"D08FQn8-eil7ImA9WxZbFEw." - * - * @param etag - */ - private void setHeaderWeakEtag(HttpUriRequest httpget, String etag) { - if (etag != null && !etag.equals("")) { - httpget.setHeader("If-None-Match", etag); - - //Log.d(TAG, "If-None-Match: " + etag); - } - } - - /** - * SUpports Post and Put. Anything else will not have any effect - * - * @param httppost - * @param list - */ - private void setPostBody(HttpUriRequest httppost, GoogleTaskList list) { - StringEntity se = null; - try { - se = new StringEntity(list.toJSON(), HTTP.UTF_8); - } - catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - se.setContentType("application/json"); - if (httppost instanceof HttpPost) - ((HttpPost) httppost).setEntity(se); - else if (httppost instanceof HttpPut) - ((HttpPut) httppost).setEntity(se); - } - - /** - * SUpports Post and Put. Anything else will not have any effect - * - * @param httppost - * @param task - */ - private void setPostBody(HttpUriRequest httppost, GoogleTask task) { - StringEntity se = null; - try { - se = new StringEntity(task.toJSON(), HTTP.UTF_8); - //Log.d(TAG, "Sending: " + task.toJSON()); - } - catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - se.setContentType("application/json"); - if (httppost instanceof HttpPost) - ((HttpPost) httppost).setEntity(se); - else if (httppost instanceof HttpPut) - ((HttpPut) httppost).setEntity(se); - } - - /** - * Parses a httpresponse and returns the string body of it. Throws - * exceptions for select status codes. - * - * @throws ClientProtocolException - * @throws PreconditionException - */ - private static String parseResponse(HttpResponse response) - throws ClientProtocolException, PreconditionException { - String page = ""; - BufferedReader in = null; - - Log.d(TAG, "HTTP Response Code: " - + response.getStatusLine().getStatusCode()); - - if (response.getStatusLine().getStatusCode() == 403) { - // Invalid authtoken - throw new ClientProtocolException("Status: 403, Invalid authcode"); - } - - else if (response.getStatusLine().getStatusCode() == 412) { // - /* - * Precondition failed. Object has been modified on server, can't do - * update - */ - throw new PreconditionException( - "Etags don't match, can not perform update. Resolve the conflict then update without etag"); - } - - /* - * else if (response.getStatusLine().getStatusCode() == 304) { throw new - * NotModifiedException(); } - */ - else if (response.getStatusLine().getStatusCode() == 400) { - // Warning: can happen for a legitimate case - // This happens if you try to delete the default list. - // Resolv it by considering the delete successful. List will still - // exist on server, but all tasks will be deleted from it. - // A successful delete returns an empty response. - // Make a log entry about it anyway though - Log.d(TAG, - "Response was 400. Either we deleted the default list in app or did something really bad"); - throw new PreconditionException( - "Tried to delete default list, undelete it"); - } - else if (response.getStatusLine().getStatusCode() == 204) { - // Successful delete of a tasklist. return empty string as that is - // expected from delete - - Log.d(TAG, "Response was 204: Successful delete"); - return ""; - } - else { - - try { - if (response.getEntity() != null) { - // Only call getContent ONCE - InputStream content = AndroidHttpClient - .getUngzippedContent(response.getEntity()); - if (content != null) { - in = new BufferedReader(new InputStreamReader(content)); - StringBuilder sb = new StringBuilder(""); - String line; - String NL = System.getProperty("line.separator"); - while ((line = in.readLine()) != null) { - sb.append(line).append(NL); - } - in.close(); - page = sb.toString(); - // - // System.out.println(page); - } - } - } - catch (IOException ignored) { - } - finally { - if (in != null) { - try { - in.close(); - } - catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - return page; - } -} diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTask.java b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTask.java index 5187bb9e8..9c44d8e72 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTask.java +++ b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTask.java @@ -16,17 +16,9 @@ package com.nononsenseapps.notepad.sync.googleapi; -import java.text.ParseException; -import java.util.Comparator; -import java.util.HashMap; - -import org.json.JSONException; -import org.json.JSONObject; - import com.nononsenseapps.notepad.database.LegacyDBHelper.NotePad; import com.nononsenseapps.notepad.database.RemoteTask; import com.nononsenseapps.notepad.database.Task; -import com.nononsenseapps.util.BiMap; import com.nononsenseapps.utils.time.RFC3339Date; import android.content.ContentValues; @@ -36,37 +28,6 @@ public class GoogleTask extends RemoteTask { - /* - public static class RemoteOrder implements Comparator { - - final HashMap levels; - - public RemoteOrder(final HashMap levels) { - this.levels = levels; - } - - @Override - public int compare(GoogleTask lhs, GoogleTask rhs) { - Log.d("remotesort", "Comparing: " + lhs.title + " and " + rhs.title); - final Integer leftLevel = levels.get(lhs.id); - final Integer rightLevel = levels.get(rhs.id); - - Log.d("remotesort", "lhs level: " + leftLevel + ", rhs level: " + rightLevel); - - if (leftLevel == null || rightLevel == null) - return 0; - - if (leftLevel == rightLevel) { - // Share parents, compare their positions - return lhs.position.compareTo(rhs.position); - } else if (leftLevel < rightLevel) { - return -1; - } else { - return 1; - } - } - }*/ - private static final String TAG = "nononsenseapps GoogleTask"; public static final String ID = "id"; public static final String TITLE = "title"; @@ -103,42 +64,48 @@ public GoogleTask(final String accountName) { this.service = GoogleTaskList.SERVICENAME; } - public GoogleTask(final JSONObject jsonTask, final String accountName) throws JSONException { + public GoogleTask(final GoogleTasksAPI.TaskResource taskResource, final String accountName) { super(); this.service = GoogleTaskList.SERVICENAME; account = accountName; - remoteId = jsonTask.getString(ID); - try { - updated = RFC3339Date.parseRFC3339Date(jsonTask.getString(UPDATED)).getTime(); - } - catch (Exception e) { - updated = 0L; - } - //etag = jsonTask.getString("etag"); - - if (jsonTask.has(TITLE)) - title = jsonTask.getString(TITLE); - if (jsonTask.has(NOTES)) - notes = jsonTask.getString(NOTES); - if (jsonTask.has(STATUS)) - status = jsonTask.getString(STATUS); - if (jsonTask.has(PARENT)) - parent = jsonTask.getString(PARENT); - else - parent = null; - if (jsonTask.has(POSITION)) - position = jsonTask.getString(POSITION); - if (jsonTask.has(DUE)) - dueDate = jsonTask.getString(DUE); - if (jsonTask.has(DELETED) && jsonTask.getBoolean(DELETED)) - remotelydeleted = true; - if (jsonTask.has(HIDDEN) && jsonTask.getBoolean(HIDDEN)) - remotelydeleted = true; -// json = jsonTask; + updateFromTaskResource(taskResource); } - public GoogleTask(final Task dbTask, final String accountName) { + /** + * Fill in fields from taskresource + */ + public void updateFromTaskResource(GoogleTasksAPI.TaskResource taskResource) { + remoteId = taskResource.id; + try { + updated = RFC3339Date.parseRFC3339Date(taskResource.updated).getTime(); + } + catch (Exception e) { + updated = 0L; + } + //etag = jsonTask.getString("etag"); + + if (taskResource.title != null) + title = taskResource.title; + if (taskResource.notes != null) + notes = taskResource.notes; + if (taskResource.status != null) + status = taskResource.status; + if (taskResource.parent != null) + parent = taskResource.parent; + else + parent = null; + if (taskResource.position != null) + position = taskResource.position; + if (taskResource.due != null) + dueDate = taskResource.due; + if (taskResource.deleted != null && taskResource.deleted) + remotelydeleted = true; + if (taskResource.hidden != null && taskResource.hidden) + remotelydeleted = true; + } + + public GoogleTask(final Task dbTask, final String accountName) { super(); this.service = GoogleTaskList.SERVICENAME; account = accountName; @@ -164,44 +131,17 @@ public void fillFrom(final Task dbTask) { } /** - * Special tricks because google api actually want 'null' while JSONObject - * doesnt allow them. do not include read-only fields - * - * @return + * Return a taskresource version of this task. Does not include id. */ - public String toJSON() { - String returnString = ""; - try { - JSONObject json = new JSONObject(); - String nullAppendage = ""; - // if (id != null) - // json.put(ID, id); - - json.put(TITLE, title); - json.put(NOTES, notes); - - if (dueDate != null && !dueDate.equals("")) - json.put(DUE, dueDate); - else - nullAppendage += ", \"" + DUE + "\": null"; + public GoogleTasksAPI.TaskResource toTaskResource() { + GoogleTasksAPI.TaskResource result = new GoogleTasksAPI.TaskResource(); - json.put(STATUS, status); - if (status != null && status.equals(NEEDSACTION)) { - // We must reset this also in this case - nullAppendage += ", \"" + COMPLETED + "\": null"; - } - - nullAppendage += "}"; - - String jsonString = json.toString(); - returnString = jsonString.substring(0, jsonString.length() - 1) - + nullAppendage; - - } catch (JSONException e) { - Log.d(TAG, e.getLocalizedMessage()); - } + result.title = title; + result.notes = notes; + result.due = dueDate; + result.status = status; - return returnString; + return result; } /** diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskList.java b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskList.java index 911080584..05fd81918 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskList.java +++ b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskList.java @@ -16,22 +16,11 @@ package com.nononsenseapps.notepad.sync.googleapi; -import java.io.IOException; -import java.text.ParseException; -import java.util.ArrayList; - -import org.apache.http.client.ClientProtocolException; -import org.json.JSONException; -import org.json.JSONObject; - import com.nononsenseapps.notepad.database.RemoteTaskList; import com.nononsenseapps.notepad.database.TaskList; -import com.nononsenseapps.util.BiMap; import com.nononsenseapps.utils.time.RFC3339Date; -import android.content.ContentValues; import android.database.Cursor; -import android.os.RemoteException; import com.nononsenseapps.helpers.Log; public class GoogleTaskList extends RemoteTaskList { @@ -53,25 +42,13 @@ public class GoogleTaskList extends RemoteTaskList { // private GoogleAPITalker api; - public GoogleTaskList(final JSONObject jsonList, final String accountName) throws JSONException { - super(); - this.service = SERVICENAME; - // this.api = ; - - remoteId = jsonList.getString("id"); - title = jsonList.getString("title"); - account = accountName; - - try { - updated = RFC3339Date.parseRFC3339Date(jsonList.getString("updated")).getTime(); - } - catch (Exception e) { - Log.d(TAG, e.getLocalizedMessage()); - updated = 0L; - } + public GoogleTaskList(GoogleTasksAPI.TaskListResource taskListResource, String accountName) { + super(); + this.service = SERVICENAME; + account = accountName; - //json = jsonList; - } + updateFromTaskListResource(taskListResource); + } public GoogleTaskList(final TaskList dbList, final String accountName) { super(); @@ -92,50 +69,38 @@ public GoogleTaskList(final Cursor c) { this.service = SERVICENAME; } -// public String toString() { -// String res = ""; -// JSONObject json = new JSONObject(); -// try { -// json.put("title", title); -// json.put("id", remoteId); -// // json.put("etag", etag); -// json.put("dbid", dbId); -// json.put("deleted", deleted); -// json.put("updated", updated); -// -// res = json.toString(2); -// } catch (JSONException e) { -// Log.d(TAG, e.getLocalizedMessage()); -// } -// return res; -// } - public GoogleTaskList(final Long dbid, final String remoteId, final Long updated, final String account) { super(dbid, remoteId, updated, account); this.service = SERVICENAME; } - /** - * Returns a JSON formatted version of this list. Includes title and not id - * - * @return - * @throws JSONException + /** + * Includes title and not id */ - public String toJSON() { - JSONObject json = new JSONObject(); - try { - json.put("title", title); + public GoogleTasksAPI.TaskListResource toTaskListResource() { + GoogleTasksAPI.TaskListResource taskListResource = new GoogleTasksAPI.TaskListResource(); - // if (id != null) - // json.put("id", id); + taskListResource.title = title; - } catch (JSONException e) { - Log.d(TAG, e.getLocalizedMessage()); - } - - return json.toString(); + return taskListResource; } + /** + * Update all fields from the resource + */ + public void updateFromTaskListResource(GoogleTasksAPI.TaskListResource taskListResource) { + remoteId = taskListResource.id; + title = taskListResource.title; + + try { + updated = RFC3339Date.parseRFC3339Date(taskListResource.updated).getTime(); + } + catch (Exception e) { + Log.d(TAG, e.getLocalizedMessage()); + updated = 0L; + } + } + /** * Returns true if the TaskList has the same remote id or the same database * id. diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskSync.java b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskSync.java index 31bba0f64..77420b39e 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskSync.java +++ b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTaskSync.java @@ -27,11 +27,11 @@ import android.preference.PreferenceManager; import android.util.Pair; +import com.nononsenseapps.build.Config; import com.nononsenseapps.helpers.Log; import com.nononsenseapps.notepad.database.Task; import com.nononsenseapps.notepad.database.TaskList; import com.nononsenseapps.notepad.prefs.SyncPrefs; -import com.nononsenseapps.notepad.sync.googleapi.GoogleAPITalker.PreconditionException; import com.nononsenseapps.utils.time.RFC3339Date; import org.apache.http.client.ClientProtocolException; @@ -43,9 +43,10 @@ import java.util.HashMap; import java.util.List; +import retrofit.RetrofitError; + public class GoogleTaskSync { static final String TAG = "nononsenseapps gtasksync"; - public static final String AUTH_TOKEN_TYPE = "Manage your tasks"; public static final boolean NOTIFY_AUTH_FAILURE = true; public static final String PREFS_LAST_SYNC_ETAG = "lastserveretag"; public static final String PREFS_GTASK_LAST_SYNC_TIME = "gtasklastsync"; @@ -64,131 +65,104 @@ public static boolean fullSync(final Context context, boolean success = false; // Initialize necessary stuff final AccountManager accountManager = AccountManager.get(context); - final GoogleAPITalker apiTalker = new GoogleAPITalker(context); - - try { - boolean connected = apiTalker.initialize(accountManager, account, - AUTH_TOKEN_TYPE, NOTIFY_AUTH_FAILURE); - if (connected) { + try { + GoogleTasksClient client = new GoogleTasksClient(GoogleTasksClient.getAuthToken + (accountManager, account, NOTIFY_AUTH_FAILURE), Config + .getGtasksApiKey(context), account.name); - Log.d(TAG, "AuthToken acquired, we are connected..."); + Log.d(TAG, "AuthToken acquired, we are connected..."); - try { - // IF full sync, download since start of all time - // Temporary fix for delete all bug + // IF full sync, download since start of all time + // Temporary fix for delete all bug // if (PreferenceManager.getDefaultSharedPreferences(context) // .getBoolean(SyncPrefs.KEY_FULLSYNC, false)) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putBoolean(SyncPrefs.KEY_FULLSYNC, false) - .putLong(PREFS_GTASK_LAST_SYNC_TIME, 0) - .commit(); + PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SyncPrefs + .KEY_FULLSYNC, false).putLong(PREFS_GTASK_LAST_SYNC_TIME, 0).commit(); // } - // Download lists from server - Log.d(TAG, "download lists"); - final List remoteLists = downloadLists(apiTalker); - - // merge with local complement - Log.d(TAG, "merge lists"); - mergeListsWithLocalDB(context, account.name, remoteLists); - - // Synchronize lists locally - Log.d(TAG, "sync lists locally"); - final List> listPairs = synchronizeListsLocally( - context, remoteLists); - - // Synchronize lists remotely - Log.d(TAG, "sync lists remotely"); - final List> syncedPairs = synchronizeListsRemotely( - context, listPairs, apiTalker); - - // For each list - for (Pair syncedPair : syncedPairs) { - // Download tasks from server - Log.d(TAG, "download tasks"); - final List remoteTasks = downloadChangedTasks( - context, apiTalker, syncedPair.second); - - // merge with local complement - Log.d(TAG, "merge tasks"); - mergeTasksWithLocalDB(context, account.name, - remoteTasks, syncedPair.first._id); - - // Synchronize tasks locally - Log.d(TAG, "sync tasks locally"); - final List> taskPairs = synchronizeTasksLocally( - context, remoteTasks, syncedPair); - // Synchronize tasks remotely - Log.d(TAG, "sync tasks remotely"); - synchronizeTasksRemotely(context, taskPairs, - syncedPair.second, apiTalker); - } - - Log.d(TAG, "Sync Complete!"); - success = true; - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putLong(PREFS_GTASK_LAST_SYNC_TIME, startTime) - .commit(); - - /* - * Tasks Step 1: Download changes from the server Step 2: - * Iterate and compare with local content Step 2a: If both - * versions changed, choose the latest Step 2b: If remote is - * newer, put info in local task, save Step 2c: If local is - * newer, upload it (in background) Step 3: For remote items - * that do not exist locally, save Step 4: For local items - * that do not exist remotely, upload - */ - - } - catch (ClientProtocolException e) { - - Log.e(TAG, - "ClientProtocolException: " - + e.getLocalizedMessage()); - syncResult.stats.numAuthExceptions++; - } - catch (IOException e) { - syncResult.stats.numIoExceptions++; - - Log.e(TAG, "IOException: " + e.getLocalizedMessage()); - } - catch (ClassCastException e) { - // GetListofLists will cast this if it returns a string. - // It should not return a string but it did... - syncResult.stats.numAuthExceptions++; - Log.e(TAG, "ClassCastException: " + e.getLocalizedMessage()); - } - - } - else { - // return real failure - - Log.d(TAG, "Could not get authToken. Reporting authException"); - syncResult.stats.numAuthExceptions++; - // doneIntent.putExtra(SYNC_RESULT, LOGIN_FAIL); - } - - } - catch (Exception e) { - // Something went wrong, don't punish the user - syncResult.stats.numAuthExceptions++; - Log.e(TAG, "bobs your uncle: " + e.getLocalizedMessage()); - } - finally { - // This must always be called or we will leak resources - if (apiTalker != null) { - apiTalker.closeClient(); - } - - Log.d(TAG, "SyncResult: " + syncResult.toDebugString()); - } - - return success; - } + // Download lists from server + Log.d(TAG, "download lists"); + final List remoteLists = downloadLists(client); + + // merge with local complement + Log.d(TAG, "merge lists"); + mergeListsWithLocalDB(context, account.name, remoteLists); + + // Synchronize lists locally + Log.d(TAG, "sync lists locally"); + final List> listPairs = synchronizeListsLocally + (context, remoteLists); + + // Synchronize lists remotely + Log.d(TAG, "sync lists remotely"); + final List> syncedPairs = synchronizeListsRemotely + (context, listPairs, client); + + // For each list + for (Pair syncedPair : syncedPairs) { + // Download tasks from server + Log.d(TAG, "download tasks"); + final List remoteTasks = downloadChangedTasks(context, client, + syncedPair.second); + + // merge with local complement + Log.d(TAG, "merge tasks"); + mergeTasksWithLocalDB(context, account.name, remoteTasks, syncedPair.first._id); + + // Synchronize tasks locally + Log.d(TAG, "sync tasks locally"); + final List> taskPairs = synchronizeTasksLocally(context, + remoteTasks, syncedPair); + // Synchronize tasks remotely + Log.d(TAG, "sync tasks remotely"); + synchronizeTasksRemotely(context, taskPairs, syncedPair.second, client); + } + + Log.d(TAG, "Sync Complete!"); + success = true; + PreferenceManager.getDefaultSharedPreferences(context).edit().putLong + (PREFS_GTASK_LAST_SYNC_TIME, startTime).commit(); + } catch (RetrofitError e) { + Log.d(TAG, "Retrofit: " + e); + final int status; + if (e.getResponse() != null) { + Log.e(TAG, "" + + e.getResponse().getStatus() + + "; " + + e.getResponse().getReason()); + status = e.getResponse().getStatus(); + } else { + status = 999; + } + // An HTTP error was encountered. + switch (status) { + case 401: // Unauthorized, token could possibly just be stale + // auth-exceptions are hard errors, and if the token is stale, + // that's too harsh + //syncResult.stats.numAuthExceptions++; + // Instead, report ioerror, which is a soft error + syncResult.stats.numIoExceptions++; + break; + case 404: // No such item, should never happen, programming error + case 415: // Not proper body, programming error + case 400: // Didn't specify url, programming error + syncResult.databaseError = true; + break; + default: // Default is to consider it a networking/server issue + syncResult.stats.numIoExceptions++; + break; + } + } catch (Exception e) { + // Something went wrong, don't punish the user + Log.e(TAG, "Exception: " + e); + syncResult.stats.numIoExceptions++; + } finally { + Log.d(TAG, "SyncResult: " + syncResult.toDebugString()); + } + + return success; + } /** * Loads the remote lists from the database and merges the two lists. If the @@ -211,7 +185,7 @@ public static void mergeListsWithLocalDB(final Context context, + GoogleTaskList.Columns.SERVICE + " IS ?", new String[] { account, GoogleTaskList.SERVICENAME }, null); try { - while (c.moveToNext()) { + while (c != null && c.moveToNext()) { GoogleTaskList list = new GoogleTaskList(c); localVersions.put(list.remoteId, list); } @@ -259,7 +233,7 @@ public static void mergeTasksWithLocalDB(final Context context, new String[] { Long.toString(listDbId), account, GoogleTaskList.SERVICENAME }, null); try { - while (c.moveToNext()) { + while (c != null && c.moveToNext()) { GoogleTask task = new GoogleTask(c); localVersions.put(task.remoteId, task); } @@ -297,12 +271,14 @@ public static void mergeTasksWithLocalDB(final Context context, * @throws IOException * @throws ClientProtocolException * @throws JSONException + * @param client */ - static List downloadLists(final GoogleAPITalker apiTalker) - throws ClientProtocolException, IOException, JSONException { + static List downloadLists(final GoogleTasksClient client) + throws IOException, RetrofitError { // Do the actual download final ArrayList remoteLists = new ArrayList(); - apiTalker.getListOfLists(remoteLists); + + client.listLists(remoteLists); // Return list of TaskLists return remoteLists; @@ -392,8 +368,7 @@ else if (localList.updated.equals(remoteList.updated)) { static List> synchronizeListsRemotely( final Context context, final List> listPairs, - final GoogleAPITalker apiTalker) throws ClientProtocolException, - IOException, PreconditionException, JSONException { + final GoogleTasksClient client) throws IOException, RetrofitError { final List> syncedPairs = new ArrayList>(); // For every list for (final Pair pair : listPairs) { @@ -401,8 +376,8 @@ static List> synchronizeListsRemotely( if (pair.second == null) { // New list to create final GoogleTaskList newList = new GoogleTaskList(pair.first, - apiTalker.accountName); - apiTalker.uploadList(newList); + client.accountName); + client.insertList(newList); // Save to db also newList.save(context); pair.first.save(context, newList.updated); @@ -414,10 +389,14 @@ else if (pair.second.isDeleted()) { // Deleted locally, delete remotely also pair.second.remotelyDeleted = true; try { - apiTalker.uploadList(pair.second); + client.deleteList(pair.second); } - catch (PreconditionException e) { - // Deleted the default list. Ignore error + catch (RetrofitError e) { + if (e.getResponse() != null && e.getResponse().getStatus() == 412) { + // Deleted the default list. Ignore error (Precondition Error) + } else { + throw e; + } } // and delete from db if it exists there pair.second.delete(context); @@ -426,7 +405,7 @@ else if (pair.second.isDeleted()) { else if (pair.first.updated > pair.second.updated) { // If local update is different than remote, that means we // should update - apiTalker.uploadList(pair.second); + client.patchList(pair.second); // No need to save remote object pair.first.save(context, pair.second.updated); } @@ -441,17 +420,16 @@ else if (pair.first.updated > pair.second.updated) { static void synchronizeTasksRemotely(final Context context, final List> taskPairs, - final GoogleTaskList gTaskList, final GoogleAPITalker apiTalker) - throws ClientProtocolException, IOException, PreconditionException, - JSONException { + final GoogleTaskList gTaskList, final GoogleTasksClient client) + throws IOException, RetrofitError { for (final Pair pair : taskPairs) { // if newly created locally if (pair.second == null) { Log.d(TAG, "Second was null"); final GoogleTask newTask = new GoogleTask(pair.first, - apiTalker.accountName); - apiTalker.uploadTask(newTask, gTaskList); + client.accountName); + client.insertTask(newTask, gTaskList); newTask.save(context); pair.first.save(context, newTask.updated); } @@ -460,7 +438,7 @@ else if (pair.second.isDeleted()) { Log.d(TAG, "Second isDeleted"); // Delete remote also pair.second.remotelydeleted = true; - apiTalker.uploadTask(pair.second, gTaskList); + client.deleteTask(pair.second, gTaskList); // Remove from db pair.second.delete(context); } @@ -468,7 +446,7 @@ else if (pair.second.isDeleted()) { // should update remote else if (pair.first.updated > pair.second.updated) { Log.d(TAG, "First updated after second"); - apiTalker.uploadTask(pair.second, gTaskList); + client.patchTask(pair.second, gTaskList); // No need to save remote object here pair.first.save(context, pair.second.updated); } @@ -484,7 +462,7 @@ static TaskList loadRemoteListFromDB(final Context context, null, null, null); TaskList tl = null; try { - if (c.moveToFirst()) { + if (c != null && c.moveToFirst()) { tl = new TaskList(c); } } @@ -503,7 +481,7 @@ static List loadNewListsFromDB(final Context context, remoteList.getTaskListWithoutRemoteArgs(), null); final ArrayList lists = new ArrayList(); try { - while (c.moveToNext()) { + while (c != null && c.moveToNext()) { lists.add(new TaskList(c)); } } @@ -524,7 +502,7 @@ static List loadNewTasksFromDB(final Context context, GoogleTaskList.SERVICENAME), null); final ArrayList tasks = new ArrayList(); try { - while (c.moveToNext()) { + while (c != null && c.moveToNext()) { tasks.add(new Task(c)); } } @@ -536,17 +514,14 @@ static List loadNewTasksFromDB(final Context context, } static List downloadChangedTasks(final Context context, - final GoogleAPITalker apiTalker, final GoogleTaskList remoteList) - throws ClientProtocolException, IOException, JSONException { + final GoogleTasksClient client, final GoogleTaskList remoteList) + throws IOException, RetrofitError { // final SharedPreferences settings = PreferenceManager // .getDefaultSharedPreferences(context); // RFC3339Date.asRFC3339(settings.getLong( // PREFS_GTASK_LAST_SYNC_TIME, 0)) - final List remoteTasks = apiTalker.getModifiedTasks( - null, remoteList); - - return remoteTasks; + return client.listTasks(remoteList); } static Task loadRemoteTaskFromDB(final Context context, @@ -556,7 +531,7 @@ static Task loadRemoteTaskFromDB(final Context context, remoteTask.getTaskWithRemoteArgs(), null); Task t = null; try { - if (c.moveToFirst()) { + if (c != null && c.moveToFirst()) { t = new Task(c); } } @@ -605,7 +580,7 @@ else if (remoteTask.isDeleted()) { try { localTask.due = RFC3339Date.combineDateAndTime(remoteTask.dueDate, localTask.due); } - catch (Exception e) { + catch (Exception ignored) { } } if (remoteTask.status != null @@ -698,43 +673,4 @@ else if (localTask != null && remoteTask != null // return pairs return taskPairs; } - - // private void sortByRemoteParent(final ArrayList tasks) { - // final HashMap levels = new HashMap(); - // levels.put(null, -1); - // final ArrayList tasksToDo = (ArrayList) tasks - // .clone(); - // GoogleTask lastFailed = null; - // int current = -1; - // Log.d("remoteorder", "Doing remote sorting with size: " + tasks.size()); - // while (!tasksToDo.isEmpty()) { - // current = current >= (tasksToDo.size() - 1) ? 0 : current + 1; - // Log.d("remoteorder", "current: " + current); - // - // if (levels.containsKey(tasksToDo.get(current).parent)) { - // Log.d("remoteorder", "parent in levelmap"); - // levels.put(tasksToDo.get(current).id, - // levels.get(tasksToDo.get(current).parent) + 1); - // tasksToDo.remove(current); - // current -= 1; - // lastFailed = null; - // } - // else if (lastFailed == null) { - // Log.d("remoteorder", "lastFailed null, now " + current); - // lastFailed = tasksToDo.get(current); - // } - // else if (lastFailed.equals(tasksToDo.get(current))) { - // Log.d("remoteorder", "lastFailed == current"); - // // Did full lap, parent is not new - // levels.put(tasksToDo.get(current).id, 99); - // levels.put(tasksToDo.get(current).parent, 98); - // tasksToDo.remove(current); - // current -= 1; - // lastFailed = null; - // } - // } - // - // // Just to make sure that new notes appear first in insertion order - // Collections.sort(tasks, new GoogleTask.RemoteOrder(levels)); - // } } diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTasksAPI.java b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTasksAPI.java new file mode 100644 index 000000000..f2193a14f --- /dev/null +++ b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTasksAPI.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2015 Jonas Kalderstam. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nononsenseapps.notepad.sync.googleapi; + +import java.util.List; + +import retrofit.http.Body; +import retrofit.http.DELETE; +import retrofit.http.GET; +import retrofit.http.PATCH; +import retrofit.http.POST; +import retrofit.http.Path; +import retrofit.http.Query; + +/** + * Google Tasks REST API. + */ +public interface GoogleTasksAPI { + + @GET("/users/@me/lists") + ListListsResponse listLists(@Query("key") String key); + + @GET("/users/@me/lists") + ListListsResponse listLists(@Query("key") String key, @Query("pageToken") String pageToken); + + // @Query("updatedMin") String updatedMin + @GET("/lists/{tasklist}/tasks") + ListTasksResponse listTasks(@Path("tasklist") String tasklist, @Query("key") String key, + @Query("showDeleted") boolean showDeleted); + + // @Query("updatedMin") String updatedMin + @GET("/lists/{tasklist}/tasks") + ListTasksResponse listTasks(@Path("tasklist") String tasklist, @Query("key") String key, + @Query("showDeleted") boolean showDeleted, @Query("pageToken") String pageToken); + + // Lists + @POST("/users/@me/lists") + TaskListResource insertList(@Body TaskListResource taskListResource, @Query("key") String key); + + @PATCH("/users/@me/lists/{tasklist}") + TaskListResource patchList(@Path("tasklist") String tasklist, @Body TaskListResource + taskListResource, @Query("key") String key); + + @DELETE("/users/@me/lists/{tasklist}") + VoidResponse deleteList(@Path("tasklist") String tasklist, @Query("key") String key); + + // Tasks + @POST("/lists/{tasklist}/tasks") + TaskResource insertTask(@Path("tasklist") String tasklist, @Body TaskResource taskResource, + @Query("key") String key); + + @PATCH("/lists/{tasklist}/tasks/{task}") + TaskResource patchTask(@Path("tasklist") String tasklist, @Path("task") String task, @Body + TaskResource taskResource, @Query("key") String key); + + @DELETE("/lists/{tasklist}/tasks/{task}") + VoidResponse deleteTask(@Path("tasklist") String tasklist, @Path("task") String task, @Query("key") + String key); + + class ListListsResponse { + String etag; + String nextPageToken; + List items; + } + + class ListTasksResponse { + String etag; + String nextPageToken; + List items; + } + + class TaskResource { + // Task identifier. + public String id; + // ETag of the resource. + public String etag; + // Title of the task. + public String title; + // Last modification time of the task (as a RFC 3339 timestamp). + public String updated; + // URL pointing to this task. + public String selfLink; + // Parent task identifier. This field is omitted if it is a top-level task. This field is + // read-only. Use the "move" method to move the task under a different parent or to the + // top level. + public String parent; + // indicating the position of the task among its sibling tasks under the same parent task + // or at the top level. If this string is greater than another task's corresponding + // position string according to lexicographical ordering, the task is positioned after + // the other task under the same parent task (or at the top level). This field is + // read-only. Use the "move" method to move the task to another position. + public String position; + // Notes describing the task. Optional. + public String notes; + // Status of the task. This is either "needsAction" or "completed". + public String status; + // Due date of the task (as a RFC 3339 timestamp). Optional. + public String due; + // Flag indicating whether the task has been deleted. The default if False. + public Boolean deleted; + // Flag indicating whether the task is hidden. This is the case if the task had been + // marked completed when the task list was last cleared. The default is False. This field + // is read-only. + public Boolean hidden; + } + + class TaskListResource { + public String id; // Task list identifier. + public String etag; // ETag of the resource. + public String title; // Title of the task list. + public String selfLink; // URL pointing to this task list. + public String updated; // Last modification time of the task list (as a RFC 3339 timestamp). + } + + class VoidResponse { + } +} diff --git a/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTasksClient.java b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTasksClient.java new file mode 100644 index 000000000..f08e0073f --- /dev/null +++ b/app/src/main/java/com/nononsenseapps/notepad/sync/googleapi/GoogleTasksClient.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2015 Jonas Kalderstam. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nononsenseapps.notepad.sync.googleapi; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.os.Bundle; + +import com.nononsenseapps.helpers.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import retrofit.RequestInterceptor; +import retrofit.RestAdapter; + +/** + * Communication client with Google Tasks API. + */ +public class GoogleTasksClient { + static final String BASE_URL = "https://www.googleapis.com/tasks/v1"; + // https://www.googleapis.com/auth/tasks.readonly + private static final String OAUTH_SCOPE = "oauth2:https://www.googleapis.com/auth/tasks"; + private static final String TAG = "GoogleTasksClient"; + final GoogleTasksAPI api; + final String accountName; + private final String key; + + public GoogleTasksClient(final String token, final String key, final String accountName) { + this.api = GetGoogleTasksAPI(token); + this.key = key; + this.accountName = accountName; + } + + public static String getAuthToken(AccountManager accountManager, Account account, boolean notifyAuthFailure) throws AuthenticatorException, OperationCanceledException, IOException { + Log.d(TAG, "getAuthToken"); + String authToken = null; + // Might be invalid in the cache + authToken = accountManager.blockingGetAuthToken(account, OAUTH_SCOPE, notifyAuthFailure); + + Log.d(TAG, "invalidate auth token: " + authToken); + accountManager.invalidateAuthToken("com.google", authToken); + + authToken = accountManager.blockingGetAuthToken(account, OAUTH_SCOPE, notifyAuthFailure); + Log.d(TAG, "fresh auth token: " + authToken); + + return authToken; + } + + /** + * Get an AuthToken asynchronously. Use this in a foreground activity which will ask the user + * for permission. + */ + public static void getAuthTokenAsync(Activity activity, Account account, + AccountManagerCallback callback) { + Log.d(TAG, "getAuthTokenAsync"); + AccountManager.get(activity).getAuthToken(account, OAUTH_SCOPE, Bundle.EMPTY, activity, + callback, null); + } + + static GoogleTasksAPI GetGoogleTasksAPI(final String token) throws IllegalArgumentException { + if (token == null || token.isEmpty()) { + throw new IllegalArgumentException("Auth token can't be empty!"); + } + Log.d(TAG, "Using token: " + token); + // Create a very simple REST adapter, with oauth header + RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(BASE_URL) + .setRequestInterceptor(new RequestInterceptor() { + @Override + public void intercept(RequestFacade request) { + request.addHeader("Authorization", "Bearer " + token); + } + }) + .build(); + // Create an instance of the interface + return restAdapter.create(GoogleTasksAPI.class); + } + + + public void listLists(final ArrayList remoteLists) { + GoogleTasksAPI.ListListsResponse response; + String pageToken = null; + do { + if (pageToken == null) { + response = api.listLists(key); + } else { + response = api.listLists(key, pageToken); + } + pageToken = response.nextPageToken; + + if (response.items == null) { + // No items + break; + } else { + for (GoogleTasksAPI.TaskListResource taskListResource : response.items) { + remoteLists.add(new GoogleTaskList(taskListResource, accountName)); + } + } + } while (pageToken != null); + } + + public void insertList(GoogleTaskList list) { + GoogleTasksAPI.TaskListResource result = api.insertList(list.toTaskListResource(), key); + list.updateFromTaskListResource(result); + } + + public void deleteList(GoogleTaskList list) { + api.deleteList(list.remoteId, key); + } + + public void patchList(GoogleTaskList list) { + GoogleTasksAPI.TaskListResource result = api.patchList(list.remoteId, list + .toTaskListResource(), key); + + list.updateFromTaskListResource(result); + } + + public void insertTask(GoogleTask task, GoogleTaskList taskList) { + GoogleTasksAPI.TaskResource result = api.insertTask(taskList.remoteId, task + .toTaskResource(), key); + task.updateFromTaskResource(result); + } + + public void deleteTask(GoogleTask task, GoogleTaskList taskList) { + api.deleteTask(taskList.remoteId, task.remoteId, key); + } + + public void patchTask(GoogleTask task, GoogleTaskList taskList) { + GoogleTasksAPI.TaskResource result = api.patchTask(taskList.remoteId, task.remoteId, task + .toTaskResource(), key); + task.updateFromTaskResource(result); + } + + public List listTasks(GoogleTaskList taskList) { + ArrayList remoteTasks = new ArrayList(); + GoogleTasksAPI.ListTasksResponse response; + String pageToken = null; + do { + if (pageToken == null) { + response = api.listTasks(taskList.remoteId, key, true); + } else { + response = api.listTasks(taskList.remoteId, key, true, pageToken); + } + pageToken = response.nextPageToken; + + if (response.items == null) { + // No items + break; + } else { + for (GoogleTasksAPI.TaskResource taskResource : response.items) { + GoogleTask task = new GoogleTask(taskResource, accountName); + task.listdbid = taskList.dbid; + remoteTasks.add(task); + } + } + } while (pageToken != null); + return remoteTasks; + } +}