diff --git a/app/build.gradle b/app/build.gradle index 31ceafc9f..cb92a2c2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,6 +164,7 @@ dependencies { implementation('com.wdullaer:materialdatetimepicker:4.0.1') implementation "me.leolin:ShortcutBadger:1.1.22@aar" implementation 'com.google.apis:google-api-services-tasks:v1-rev55-1.25.0' + implementation 'com.google.apis:google-api-services-drive:v3-rev136-1.25.0' implementation 'com.google.api-client:google-api-client-android:1.27.0' implementation 'com.android.billingclient:billing:1.1' implementation("android.arch.work:work-runtime:${WORK_VERSION}") { diff --git a/app/src/amazon/java/org/tasks/gtasks/PlayServices.java b/app/src/amazon/java/org/tasks/gtasks/PlayServices.java index 7c2cfbc93..8e2d615dc 100644 --- a/app/src/amazon/java/org/tasks/gtasks/PlayServices.java +++ b/app/src/amazon/java/org/tasks/gtasks/PlayServices.java @@ -1,8 +1,13 @@ package org.tasks.gtasks; import android.app.Activity; + import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity; + +import org.tasks.drive.DriveLoginActivity; +import org.tasks.play.AuthResultHandler; + import javax.inject.Inject; public class PlayServices { @@ -24,12 +29,6 @@ public class PlayServices { return null; } - public boolean clearToken(GoogleAccountCredential googleAccountCredential) { - return false; - } - - public void getAuthToken( - GtasksLoginActivity gtasksLoginActivity, - String a, - GtasksLoginActivity.AuthResultHandler authResultHandler) {} + public void getDriveAuthToken( + DriveLoginActivity driveLoginActivity, String a, AuthResultHandler authResultHandler) {} } diff --git a/app/src/generic/java/org/tasks/gtasks/PlayServices.java b/app/src/generic/java/org/tasks/gtasks/PlayServices.java index 7c2cfbc93..3a8b1405c 100644 --- a/app/src/generic/java/org/tasks/gtasks/PlayServices.java +++ b/app/src/generic/java/org/tasks/gtasks/PlayServices.java @@ -1,8 +1,12 @@ package org.tasks.gtasks; import android.app.Activity; -import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; + import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity; + +import org.tasks.drive.DriveLoginActivity; +import org.tasks.play.AuthResultHandler; + import javax.inject.Inject; public class PlayServices { @@ -24,12 +28,9 @@ public class PlayServices { return null; } - public boolean clearToken(GoogleAccountCredential googleAccountCredential) { - return false; - } + public void getTasksAuthToken( + GtasksLoginActivity gtasksLoginActivity, String a, AuthResultHandler authResultHandler) {} - public void getAuthToken( - GtasksLoginActivity gtasksLoginActivity, - String a, - GtasksLoginActivity.AuthResultHandler authResultHandler) {} + public void getDriveAuthToken( + DriveLoginActivity driveLoginActivity, String a, AuthResultHandler authResultHandler) {} } diff --git a/app/src/googleplay/AndroidManifest.xml b/app/src/googleplay/AndroidManifest.xml index b89bd80aa..c223e7b52 100644 --- a/app/src/googleplay/AndroidManifest.xml +++ b/app/src/googleplay/AndroidManifest.xml @@ -47,6 +47,10 @@ + + diff --git a/app/src/googleplay/java/org/tasks/gtasks/PlayServices.java b/app/src/googleplay/java/org/tasks/gtasks/PlayServices.java index bbe3e9c46..4cae4cbaf 100644 --- a/app/src/googleplay/java/org/tasks/gtasks/PlayServices.java +++ b/app/src/googleplay/java/org/tasks/gtasks/PlayServices.java @@ -4,19 +4,25 @@ import android.accounts.Account; import android.app.Activity; import android.content.Context; import android.widget.Toast; + import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthUtil; import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; -import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.services.drive.DriveScopes; import com.google.api.services.tasks.TasksScopes; import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity; -import java.io.IOException; -import javax.inject.Inject; + import org.tasks.R; import org.tasks.injection.ForApplication; +import org.tasks.play.AuthResultHandler; import org.tasks.preferences.Preferences; + +import java.io.IOException; + +import javax.inject.Inject; + import timber.log.Timber; public class PlayServices { @@ -76,46 +82,36 @@ public class PlayServices { return preferences.getInt(R.string.play_services_available, -1); } - public boolean clearToken(GoogleAccountCredential credential) { - try { - String token = credential.getToken(); - Timber.d("Invalidating %s", token); - GoogleAuthUtil.clearToken(context, token); - GoogleAuthUtil.getToken( - context, credential.getSelectedAccount(), "oauth2:" + TasksScopes.TASKS, null); - return true; - } catch (GoogleAuthException e) { - Timber.e(e); - return false; - } catch (IOException e) { - Timber.e(e); - return true; - } + public void getTasksAuthToken( + final Activity activity, final String accountName, final AuthResultHandler handler) { + getToken(TasksScopes.TASKS, activity, accountName, handler); + } + + public void getDriveAuthToken( + final Activity activity, final String accountName, final AuthResultHandler handler) { + getToken(DriveScopes.DRIVE_FILE, activity, accountName, handler); } - public void getAuthToken( - final Activity activity, - final String accountName, - final GtasksLoginActivity.AuthResultHandler handler) { + private void getToken(String scope, Activity activity, String accountName, AuthResultHandler handler) { final Account account = accountManager.getAccount(accountName); if (account == null) { handler.authenticationFailed( activity.getString(R.string.gtasks_error_accountNotFound, accountName)); } else { new Thread( - () -> { - try { - GoogleAuthUtil.getToken(activity, account, "oauth2:" + TasksScopes.TASKS, null); - handler.authenticationSuccessful(accountName); - } catch (UserRecoverableAuthException e) { - Timber.e(e); - activity.startActivityForResult( - e.getIntent(), GtasksLoginActivity.RC_REQUEST_OAUTH); - } catch (GoogleAuthException | IOException e) { - Timber.e(e); - handler.authenticationFailed(activity.getString(R.string.gtasks_GLA_errorIOAuth)); - } - }) + () -> { + try { + GoogleAuthUtil.getToken(activity, account, "oauth2:" + scope, null); + handler.authenticationSuccessful(accountName); + } catch (UserRecoverableAuthException e) { + Timber.e(e); + activity.startActivityForResult( + e.getIntent(), GtasksLoginActivity.RC_REQUEST_OAUTH); + } catch (GoogleAuthException | IOException e) { + Timber.e(e); + handler.authenticationFailed(activity.getString(R.string.gtasks_GLA_errorIOAuth)); + } + }) .start(); } } diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java b/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java index 4c105bd04..1af68e58f 100644 --- a/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java +++ b/app/src/main/java/com/todoroo/astrid/gtasks/api/GtasksInvoker.java @@ -1,23 +1,26 @@ package com.todoroo.astrid.gtasks.api; import android.content.Context; + import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.GenericJson; import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.ExponentialBackOff; import com.google.api.services.tasks.Tasks; import com.google.api.services.tasks.TasksRequest; import com.google.api.services.tasks.TasksScopes; import com.google.api.services.tasks.model.Task; import com.google.api.services.tasks.model.TaskList; import com.google.api.services.tasks.model.TaskLists; + +import org.tasks.BuildConfig; + import java.io.IOException; import java.util.Collections; -import org.tasks.BuildConfig; -import org.tasks.gtasks.GoogleTasksUnsuccessfulResponseHandler; -import org.tasks.gtasks.PlayServices; + import timber.log.Timber; /** @@ -28,15 +31,12 @@ import timber.log.Timber; */ public class GtasksInvoker { - private final GoogleAccountCredential credential; - private final PlayServices playServices; private final Tasks service; - public GtasksInvoker(Context context, PlayServices playServices, String account) { - this.playServices = playServices; - credential = - GoogleAccountCredential.usingOAuth2(context, Collections.singletonList(TasksScopes.TASKS)) - .setSelectedAccountName(account); + public GtasksInvoker(Context context, String account) { + GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context, Collections.singletonList(TasksScopes.TASKS)) + .setBackOff(new ExponentialBackOff.Builder().build()) + .setSelectedAccountName(account); service = new Tasks.Builder(new NetHttpTransport(), new JacksonFactory(), credential) .setApplicationName(String.format("Tasks/%s", BuildConfig.VERSION_NAME)) @@ -111,8 +111,6 @@ public class GtasksInvoker { String caller = getCaller(); Timber.d("%s request: %s", caller, request); HttpRequest httpRequest = request.buildHttpRequest(); - httpRequest.setUnsuccessfulResponseHandler( - new GoogleTasksUnsuccessfulResponseHandler(playServices, credential)); HttpResponse httpResponse = httpRequest.execute(); T response = httpResponse.parseAs(request.getResponseClass()); Timber.d("%s response: %s", caller, prettyPrint(response)); diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java b/app/src/main/java/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java index e46ec5a6d..ad656d8fb 100644 --- a/app/src/main/java/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java +++ b/app/src/main/java/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java @@ -19,6 +19,7 @@ import org.tasks.gtasks.GoogleAccountManager; import org.tasks.gtasks.PlayServices; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingAppCompatActivity; +import org.tasks.play.AuthResultHandler; /** * This activity allows users to sign in or log in to Google Tasks through the Android account @@ -59,7 +60,7 @@ public class GtasksLoginActivity extends InjectingAppCompatActivity { } private void getAuthToken(String a, final ProgressDialog pd) { - playServices.getAuthToken( + playServices.getTasksAuthToken( this, a, new AuthResultHandler() { @@ -103,11 +104,4 @@ public class GtasksLoginActivity extends InjectingAppCompatActivity { finish(); } } - - public interface AuthResultHandler { - - void authenticationSuccessful(String accountName); - - void authenticationFailed(String message); - } } diff --git a/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java b/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java index 17fca6dd4..d5a682540 100644 --- a/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java +++ b/app/src/main/java/com/todoroo/astrid/gtasks/sync/GtasksSyncService.java @@ -7,6 +7,7 @@ package com.todoroo.astrid.gtasks.sync; import android.content.Context; import android.text.TextUtils; + import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.astrid.dao.TaskDao; @@ -14,23 +15,26 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.gtasks.GtasksTaskListUpdater; import com.todoroo.astrid.gtasks.api.GtasksInvoker; import com.todoroo.astrid.gtasks.api.MoveRequest; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import javax.inject.Inject; + import org.tasks.analytics.Tracker; import org.tasks.data.GoogleTask; import org.tasks.data.GoogleTaskDao; import org.tasks.data.GoogleTaskList; import org.tasks.data.GoogleTaskListDao; import org.tasks.gtasks.GtaskSyncAdapterHelper; -import org.tasks.gtasks.PlayServices; import org.tasks.injection.ApplicationScope; import org.tasks.injection.ForApplication; import org.tasks.preferences.Preferences; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import javax.inject.Inject; + import timber.log.Timber; @ApplicationScope @@ -44,7 +48,6 @@ public class GtasksSyncService { private final GtaskSyncAdapterHelper gtaskSyncAdapterHelper; private final Tracker tracker; private final GoogleTaskDao googleTaskDao; - private final PlayServices playServices; @Inject public GtasksSyncService( @@ -54,15 +57,13 @@ public class GtasksSyncService { GtaskSyncAdapterHelper gtaskSyncAdapterHelper, Tracker tracker, GoogleTaskDao googleTaskDao, - GoogleTaskListDao googleTaskListDao, - PlayServices playServices) { + GoogleTaskListDao googleTaskListDao) { this.context = context; this.taskDao = taskDao; this.preferences = preferences; this.gtaskSyncAdapterHelper = gtaskSyncAdapterHelper; this.tracker = tracker; this.googleTaskDao = googleTaskDao; - this.playServices = playServices; new OperationPushThread(operationQueue).start(); } @@ -176,7 +177,7 @@ public class GtasksSyncService { @Override public void op() throws IOException { - GtasksInvoker invoker = new GtasksInvoker(context, playServices, googleTaskList.getAccount()); + GtasksInvoker invoker = new GtasksInvoker(context, googleTaskList.getAccount()); pushMetadataOnSave(googleTask, invoker); } } @@ -191,7 +192,7 @@ public class GtasksSyncService { @Override public void op() throws IOException { - GtasksInvoker invoker = new GtasksInvoker(context, playServices, googleTaskList.getAccount()); + GtasksInvoker invoker = new GtasksInvoker(context, googleTaskList.getAccount()); invoker.clearCompleted(googleTaskList.getRemoteId()); } } diff --git a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java index 99421fdf6..ddbc07388 100755 --- a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java @@ -7,6 +7,8 @@ import android.net.Uri; import android.os.Handler; import android.widget.Toast; +import com.google.api.services.drive.model.File; +import com.google.api.services.drive.model.FileList; import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -27,7 +29,9 @@ import org.tasks.data.TagDao; import org.tasks.data.TagDataDao; import org.tasks.data.TaskAttachmentDao; import org.tasks.data.UserActivityDao; +import org.tasks.drive.DriveInvoker; import org.tasks.files.FileHelper; +import org.tasks.gtasks.PlayServices; import org.tasks.preferences.Preferences; import java.io.IOException; @@ -47,6 +51,9 @@ import static org.tasks.date.DateTimeUtils.newDateTime; public class TasksJsonExporter { + private static final String MIME = "application/json"; + private static final String EXTENSION = ".json"; + // --- public interface private final TagDataDao tagDataDao; @@ -59,6 +66,7 @@ public class TasksJsonExporter { private final GoogleTaskListDao googleTaskListDao; private final TaskAttachmentDao taskAttachmentDao; private final CaldavDao caldavDao; + private final DriveInvoker driveInvoker; private final TaskDao taskDao; private final UserActivityDao userActivityDao; private final Preferences preferences; @@ -66,7 +74,6 @@ public class TasksJsonExporter { private int exportCount = 0; private ProgressDialog progressDialog; private Handler handler; - private Uri backupDirectory; private String latestSetVersionName; @Inject @@ -82,7 +89,8 @@ public class TasksJsonExporter { FilterDao filterDao, GoogleTaskListDao googleTaskListDao, TaskAttachmentDao taskAttachmentDao, - CaldavDao caldavDao) { + CaldavDao caldavDao, + DriveInvoker driveInvoker) { this.tagDataDao = tagDataDao; this.taskDao = taskDao; this.userActivityDao = userActivityDao; @@ -95,6 +103,7 @@ public class TasksJsonExporter { this.googleTaskListDao = googleTaskListDao; this.taskAttachmentDao = taskAttachmentDao; this.caldavDao = caldavDao; + this.driveInvoker = driveInvoker; } private static String getDateForExport() { @@ -121,7 +130,6 @@ public class TasksJsonExporter { @Nullable final ProgressDialog progressDialog) { this.context = context; this.exportCount = 0; - this.backupDirectory = preferences.getBackupDirectory(); this.latestSetVersionName = null; this.progressDialog = progressDialog; @@ -140,10 +148,19 @@ public class TasksJsonExporter { if (tasks.size() > 0) { String basename = Files.getNameWithoutExtension(filename); - Uri uri = FileHelper.newFile(context, backupDirectory, "application/json", basename, ".json"); + Uri uri = + FileHelper.newFile( + context, preferences.getBackupDirectory(), MIME, basename, EXTENSION); OutputStream os = context.getContentResolver().openOutputStream(uri); doTasksExport(os, tasks); os.close(); + if (preferences.getBoolean(R.string.p_google_drive_backup, false)) { + List files = driveInvoker.findFolder("org.tasks"); + File folder = files.isEmpty() + ? driveInvoker.createFolder("org.tasks") + : files.get(0); + driveInvoker.createFile(MIME, folder.getId(), FileHelper.getFilename(context, uri), uri); + } } if (exportType == ExportType.EXPORT_TYPE_MANUAL) { diff --git a/app/src/main/java/org/tasks/drive/DriveInvoker.java b/app/src/main/java/org/tasks/drive/DriveInvoker.java new file mode 100644 index 000000000..73280c8bc --- /dev/null +++ b/app/src/main/java/org/tasks/drive/DriveInvoker.java @@ -0,0 +1,104 @@ +package org.tasks.drive; + +import android.content.Context; +import android.net.Uri; + +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.services.drive.Drive; +import com.google.api.services.drive.DriveRequest; +import com.google.api.services.drive.DriveScopes; +import com.google.api.services.drive.model.File; + +import org.tasks.BuildConfig; +import org.tasks.R; +import org.tasks.injection.ForApplication; +import org.tasks.preferences.Preferences; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import timber.log.Timber; + +public class DriveInvoker { + + private final Drive service; + private final Context context; + + @Inject + public DriveInvoker(@ForApplication Context context, Preferences preferences) { + this.context = context; + if (preferences.getBoolean(R.string.p_google_drive_backup, false)) { + GoogleAccountCredential credential = + GoogleAccountCredential.usingOAuth2( + context, Collections.singletonList(DriveScopes.DRIVE_FILE)) + .setBackOff(new ExponentialBackOff.Builder().build()) + .setSelectedAccountName( + preferences.getStringValue(R.string.p_google_drive_backup_account)); + service = + new Drive.Builder(new NetHttpTransport(), new JacksonFactory(), credential) + .setApplicationName(String.format("Tasks/%s", BuildConfig.VERSION_NAME)) + .build(); + } else { + service = null; + } + } + + public List findFolder(String name) throws IOException { + String query = String.format("name='%s'", name); + return execute(service.files().list().setQ(query).setSpaces("drive")).getFiles(); + } + + public File createFolder(String name) throws IOException { + File folder = new File() + .setName(name) + .setMimeType("application/vnd.google-apps.folder"); + + return execute(service.files().create(folder).setFields("id")); + } + + public void createFile(String mime, String parent, String name, Uri uri) throws IOException { + File metadata = new File() + .setParents(Collections.singletonList(parent)) + .setMimeType(mime) + .setName(name); + InputStreamContent content = + new InputStreamContent(mime, context.getContentResolver().openInputStream(uri)); + execute(service.files().create(metadata, content)); + } + + private synchronized T execute(DriveRequest request) throws IOException { + String caller = getCaller(); + Timber.d("%s request: %s", caller, request); + T response = request.execute(); + Timber.d("%s response: %s", caller, prettyPrint(response)); + return response; + } + + private Object prettyPrint(T object) throws IOException { + if (BuildConfig.DEBUG) { + if (object instanceof GenericJson) { + return ((GenericJson) object).toPrettyString(); + } + } + return object; + } + + private String getCaller() { + if (BuildConfig.DEBUG) { + try { + return Thread.currentThread().getStackTrace()[4].getMethodName(); + } catch (Exception e) { + Timber.e(e); + } + } + return ""; + } +} diff --git a/app/src/main/java/org/tasks/drive/DriveLoginActivity.java b/app/src/main/java/org/tasks/drive/DriveLoginActivity.java new file mode 100644 index 000000000..d141062c3 --- /dev/null +++ b/app/src/main/java/org/tasks/drive/DriveLoginActivity.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2012 Todoroo Inc + * + *

See the file "LICENSE" for the full license governing this code. + */ +package org.tasks.drive; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; +import com.todoroo.andlib.utility.DialogUtilities; +import javax.inject.Inject; +import org.tasks.R; +import org.tasks.dialogs.DialogBuilder; +import org.tasks.gtasks.GoogleAccountManager; +import org.tasks.gtasks.PlayServices; +import org.tasks.injection.ActivityComponent; +import org.tasks.injection.InjectingAppCompatActivity; +import org.tasks.play.AuthResultHandler; +import org.tasks.preferences.Preferences; + +/** + * This activity allows users to sign in or log in to Google Tasks through the Android account + * manager + * + * @author Sam Bosley + */ +public class DriveLoginActivity extends InjectingAppCompatActivity { + + public static final int RC_REQUEST_OAUTH = 10987; + private static final int RC_CHOOSE_ACCOUNT = 10988; + @Inject DialogBuilder dialogBuilder; + @Inject GoogleAccountManager accountManager; + @Inject PlayServices playServices; + @Inject Preferences preferences; + private String accountName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent chooseAccountIntent = + android.accounts.AccountManager.newChooseAccountIntent( + null, null, new String[] {"com.google"}, false, null, null, null, null); + startActivityForResult(chooseAccountIntent, RC_CHOOSE_ACCOUNT); + } + + @Override + public void inject(ActivityComponent component) { + component.inject(this); + } + + private void getAuthToken(String account) { + final ProgressDialog pd = dialogBuilder.newProgressDialog(R.string.gtasks_GLA_authenticating); + pd.show(); + accountName = account; + getAuthToken(account, pd); + } + + private void getAuthToken(String a, final ProgressDialog pd) { + playServices.getDriveAuthToken( + this, + a, + new AuthResultHandler() { + @Override + public void authenticationSuccessful(String accountName) { + preferences.setString(R.string.p_google_drive_backup_account, accountName); + setResult(RESULT_OK); + finish(); + DialogUtilities.dismissDialog(DriveLoginActivity.this, pd); + } + + @Override + public void authenticationFailed(final String message) { + runOnUiThread( + () -> Toast.makeText(DriveLoginActivity.this, message, Toast.LENGTH_LONG).show()); + DialogUtilities.dismissDialog(DriveLoginActivity.this, pd); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == RC_CHOOSE_ACCOUNT && resultCode == RESULT_OK) { + String account = data.getStringExtra(android.accounts.AccountManager.KEY_ACCOUNT_NAME); + getAuthToken(account); + } else if (requestCode == RC_REQUEST_OAUTH && resultCode == RESULT_OK) { + final ProgressDialog pd = dialogBuilder.newProgressDialog(R.string.gtasks_GLA_authenticating); + pd.show(); + getAuthToken(accountName, pd); + } else { + // User didn't give permission--cancel + finish(); + } + } +} diff --git a/app/src/main/java/org/tasks/gtasks/CreateListDialog.java b/app/src/main/java/org/tasks/gtasks/CreateListDialog.java index f3c0540da..4cd4a1e80 100644 --- a/app/src/main/java/org/tasks/gtasks/CreateListDialog.java +++ b/app/src/main/java/org/tasks/gtasks/CreateListDialog.java @@ -6,17 +6,22 @@ import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; + import com.google.api.services.tasks.model.TaskList; import com.todoroo.astrid.gtasks.api.GtasksInvoker; -import java.io.IOException; -import javax.inject.Inject; + import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.ForApplication; import org.tasks.injection.InjectingDialogFragment; + +import java.io.IOException; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import timber.log.Timber; public class CreateListDialog extends InjectingDialogFragment { @@ -25,7 +30,6 @@ public class CreateListDialog extends InjectingDialogFragment { private static final String EXTRA_NAME = "extra_name"; @Inject DialogBuilder dialogBuilder; @Inject @ForApplication Context context; - @Inject PlayServices playServices; private CreateListDialogCallback callback; private ProgressDialog dialog; private String account; @@ -74,7 +78,7 @@ public class CreateListDialog extends InjectingDialogFragment { @Override protected TaskList doInBackground(Void... voids) { try { - return new GtasksInvoker(context, playServices, account).createGtaskList(name); + return new GtasksInvoker(context, account).createGtaskList(name); } catch (IOException e) { Timber.e(e); return null; diff --git a/app/src/main/java/org/tasks/gtasks/DeleteListDialog.java b/app/src/main/java/org/tasks/gtasks/DeleteListDialog.java index 497bd67fa..02891e54e 100644 --- a/app/src/main/java/org/tasks/gtasks/DeleteListDialog.java +++ b/app/src/main/java/org/tasks/gtasks/DeleteListDialog.java @@ -6,17 +6,22 @@ import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; + import com.todoroo.astrid.gtasks.api.GtasksInvoker; -import java.io.IOException; -import javax.inject.Inject; + import org.tasks.R; import org.tasks.data.GoogleTaskList; import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.ForApplication; import org.tasks.injection.InjectingDialogFragment; + +import java.io.IOException; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import timber.log.Timber; public class DeleteListDialog extends InjectingDialogFragment { @@ -24,7 +29,6 @@ public class DeleteListDialog extends InjectingDialogFragment { private static final String EXTRA_LIST = "extra_list"; @Inject @ForApplication Context context; @Inject DialogBuilder dialogBuilder; - @Inject PlayServices playServices; private DeleteListDialogCallback callback; private GoogleTaskList googleTaskList; private ProgressDialog dialog; @@ -70,7 +74,7 @@ public class DeleteListDialog extends InjectingDialogFragment { @Override protected Boolean doInBackground(Void... voids) { try { - new GtasksInvoker(context, playServices, googleTaskList.getAccount()) + new GtasksInvoker(context, googleTaskList.getAccount()) .deleteGtaskList(googleTaskList.getRemoteId()); return true; } catch (IOException e) { diff --git a/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java b/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java index c6d174b74..dc6d86f33 100644 --- a/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java +++ b/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java @@ -1,12 +1,10 @@ package org.tasks.gtasks; -import static org.tasks.date.DateTimeUtils.newDateTime; - import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import androidx.core.app.NotificationCompat; import android.text.TextUtils; + import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; import com.google.api.services.tasks.model.TaskList; import com.google.api.services.tasks.model.TaskLists; @@ -28,10 +26,7 @@ import com.todoroo.astrid.gtasks.sync.GtasksTaskContainer; import com.todoroo.astrid.service.TaskCreator; import com.todoroo.astrid.service.TaskDeleter; import com.todoroo.astrid.utility.Constants; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; + import org.tasks.LocalBroadcastManager; import org.tasks.R; import org.tasks.analytics.Tracker; @@ -47,8 +42,18 @@ import org.tasks.preferences.DefaultFilterProvider; import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.Preferences; import org.tasks.time.DateTime; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.core.app.NotificationCompat; import timber.log.Timber; +import static org.tasks.date.DateTimeUtils.newDateTime; + public class GoogleTaskSynchronizer { private static final String DEFAULT_LIST = "@default"; // $NON-NLS-1$ @@ -65,7 +70,6 @@ public class GoogleTaskSynchronizer { private final GoogleTaskDao googleTaskDao; private final TaskCreator taskCreator; private final DefaultFilterProvider defaultFilterProvider; - private final PlayServices playServices; private final PermissionChecker permissionChecker; private final GoogleAccountManager googleAccountManager; private final LocalBroadcastManager localBroadcastManager; @@ -86,7 +90,6 @@ public class GoogleTaskSynchronizer { GoogleTaskDao googleTaskDao, TaskCreator taskCreator, DefaultFilterProvider defaultFilterProvider, - PlayServices playServices, PermissionChecker permissionChecker, GoogleAccountManager googleAccountManager, LocalBroadcastManager localBroadcastManager, @@ -104,7 +107,6 @@ public class GoogleTaskSynchronizer { this.googleTaskDao = googleTaskDao; this.taskCreator = taskCreator; this.defaultFilterProvider = defaultFilterProvider; - this.playServices = playServices; this.permissionChecker = permissionChecker; this.googleAccountManager = googleAccountManager; this.localBroadcastManager = localBroadcastManager; @@ -182,7 +184,7 @@ public class GoogleTaskSynchronizer { return; } - GtasksInvoker gtasksInvoker = new GtasksInvoker(context, playServices, account.getAccount()); + GtasksInvoker gtasksInvoker = new GtasksInvoker(context, account.getAccount()); pushLocalChanges(account, gtasksInvoker); List gtaskLists = new ArrayList<>(); diff --git a/app/src/main/java/org/tasks/gtasks/GoogleTasksUnsuccessfulResponseHandler.java b/app/src/main/java/org/tasks/gtasks/GoogleTasksUnsuccessfulResponseHandler.java deleted file mode 100644 index c29ab77d6..000000000 --- a/app/src/main/java/org/tasks/gtasks/GoogleTasksUnsuccessfulResponseHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.tasks.gtasks; - -import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; -import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpStatusCodes; -import com.google.api.client.http.HttpUnsuccessfulResponseHandler; -import com.google.api.client.util.BackOff; -import com.google.api.client.util.ExponentialBackOff; -import com.todoroo.astrid.gtasks.api.HttpNotFoundException; -import java.io.IOException; -import timber.log.Timber; - -public class GoogleTasksUnsuccessfulResponseHandler implements HttpUnsuccessfulResponseHandler { - - private static final BackOff BACKOFF = new ExponentialBackOff.Builder().build(); - - private final PlayServices playServices; - private final GoogleAccountCredential googleAccountCredential; - private final HttpBackOffUnsuccessfulResponseHandler backoffHandler = - new HttpBackOffUnsuccessfulResponseHandler(BACKOFF); - - public GoogleTasksUnsuccessfulResponseHandler( - PlayServices playServices, GoogleAccountCredential googleAccountCredential) { - this.playServices = playServices; - this.googleAccountCredential = googleAccountCredential; - } - - @Override - public boolean handleResponse(HttpRequest request, HttpResponse response, boolean supportsRetry) - throws IOException { - HttpResponseException httpResponseException = new HttpResponseException(response); - Timber.e(httpResponseException); - if (!supportsRetry) { - return false; - } - int statusCode = response.getStatusCode(); - if ((statusCode == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED - || statusCode == HttpStatusCodes.STATUS_CODE_FORBIDDEN)) { - boolean shouldRetry = playServices.clearToken(googleAccountCredential); - if (!shouldRetry) { - return false; - } - } else if (statusCode == 400) { // bad request - throw httpResponseException; - } else if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { - throw new HttpNotFoundException(httpResponseException); - } - - return backoffHandler.handleResponse(request, response, supportsRetry); - } -} diff --git a/app/src/main/java/org/tasks/gtasks/RenameListDialog.java b/app/src/main/java/org/tasks/gtasks/RenameListDialog.java index c81a479d1..757a18eb6 100644 --- a/app/src/main/java/org/tasks/gtasks/RenameListDialog.java +++ b/app/src/main/java/org/tasks/gtasks/RenameListDialog.java @@ -6,18 +6,23 @@ import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; + import com.google.api.services.tasks.model.TaskList; import com.todoroo.astrid.gtasks.api.GtasksInvoker; -import java.io.IOException; -import javax.inject.Inject; + import org.tasks.R; import org.tasks.data.GoogleTaskList; import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.ForApplication; import org.tasks.injection.InjectingDialogFragment; + +import java.io.IOException; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import timber.log.Timber; public class RenameListDialog extends InjectingDialogFragment { @@ -26,7 +31,6 @@ public class RenameListDialog extends InjectingDialogFragment { private static final String EXTRA_LIST = "extra_list"; @Inject @ForApplication Context context; @Inject DialogBuilder dialogBuilder; - @Inject PlayServices playServices; private RenameListDialogCallback callback; private ProgressDialog dialog; private GoogleTaskList googleTaskList; @@ -75,7 +79,7 @@ public class RenameListDialog extends InjectingDialogFragment { @Override protected TaskList doInBackground(Void... voids) { try { - return new GtasksInvoker(context, playServices, googleTaskList.getAccount()) + return new GtasksInvoker(context, googleTaskList.getAccount()) .renameGtaskList(googleTaskList.getRemoteId(), name); } catch (IOException e) { Timber.e(e); diff --git a/app/src/main/java/org/tasks/injection/ActivityComponent.java b/app/src/main/java/org/tasks/injection/ActivityComponent.java index d21da9b4a..81ba6b447 100644 --- a/app/src/main/java/org/tasks/injection/ActivityComponent.java +++ b/app/src/main/java/org/tasks/injection/ActivityComponent.java @@ -24,6 +24,7 @@ import org.tasks.billing.PurchaseActivity; import org.tasks.caldav.CaldavAccountSettingsActivity; import org.tasks.caldav.CaldavCalendarSettingsActivity; import org.tasks.dashclock.DashClockSettings; +import org.tasks.drive.DriveLoginActivity; import org.tasks.files.FileExplore; import org.tasks.files.MyFilePickerActivity; import org.tasks.locale.ui.activity.TaskerCreateTaskActivity; @@ -128,4 +129,6 @@ public interface ActivityComponent { void inject(PurchaseActivity purchaseActivity); void inject(CaldavAccountSettingsActivity caldavAccountSettingsActivity); + + void inject(DriveLoginActivity driveLoginActivity); } diff --git a/app/src/main/java/org/tasks/play/AuthResultHandler.java b/app/src/main/java/org/tasks/play/AuthResultHandler.java new file mode 100644 index 000000000..b9088b4a8 --- /dev/null +++ b/app/src/main/java/org/tasks/play/AuthResultHandler.java @@ -0,0 +1,7 @@ +package org.tasks.play; + +public interface AuthResultHandler { + void authenticationSuccessful(String accountName); + + void authenticationFailed(String message); +} diff --git a/app/src/main/java/org/tasks/preferences/BasicPreferences.java b/app/src/main/java/org/tasks/preferences/BasicPreferences.java index 3c2a11dac..f8148ba8d 100644 --- a/app/src/main/java/org/tasks/preferences/BasicPreferences.java +++ b/app/src/main/java/org/tasks/preferences/BasicPreferences.java @@ -6,7 +6,9 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.widget.CheckBox; import com.google.common.base.Strings; import com.todoroo.astrid.core.OldTaskPreferences; @@ -22,7 +24,9 @@ import org.tasks.analytics.Tracking.Events; import org.tasks.billing.BillingClient; import org.tasks.billing.Inventory; import org.tasks.dialogs.DialogBuilder; +import org.tasks.drive.DriveLoginActivity; import org.tasks.files.FileHelper; +import org.tasks.gtasks.PlayServices; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.locale.Locale; @@ -58,6 +62,7 @@ public class BasicPreferences extends InjectingPreferenceActivity private static final int REQUEST_CODE_BACKUP_DIR = 10005; private static final int REQUEST_PICKER = 10006; private static final int REQUEST_LAUNCHER_PICKER = 10007; + private static final int RC_DRIVE_BACKUP = 10008; @Inject Tracker tracker; @Inject Preferences preferences; @Inject ThemeBase themeBase; @@ -68,6 +73,7 @@ public class BasicPreferences extends InjectingPreferenceActivity @Inject ThemeCache themeCache; @Inject BillingClient billingClient; @Inject Inventory inventory; + @Inject PlayServices playServices; private Bundle result; @@ -166,6 +172,21 @@ public class BasicPreferences extends InjectingPreferenceActivity initializeBackupDirectory(); + CheckBoxPreference googleDriveBackup = (CheckBoxPreference) findPreference(R.string.p_google_drive_backup); + googleDriveBackup.setChecked(preferences.getBoolean(R.string.p_google_drive_backup, false)); + googleDriveBackup + .setOnPreferenceChangeListener( + (preference, newValue) -> { + if (newValue != null && (Boolean) newValue) { + if (!playServices.refreshAndCheck()) { + playServices.resolve(this); + } else { + requestLogin(); + } + } + return false; + }); + requires( R.string.settings_localization, atLeastJellybeanMR1(), @@ -174,10 +195,15 @@ public class BasicPreferences extends InjectingPreferenceActivity //noinspection ConstantConditions if (!BuildConfig.FLAVOR.equals("googleplay")) { + remove(R.string.p_google_drive_backup); requires(R.string.privacy, false, R.string.p_collect_statistics); } } + private void requestLogin() { + startActivityForResult(new Intent(this, DriveLoginActivity.class), RC_DRIVE_BACKUP); + } + private void setupActivity(int key, final Class target) { findPreference(getString(key)) .setOnPreferenceClickListener( @@ -249,6 +275,9 @@ public class BasicPreferences extends InjectingPreferenceActivity newImportTasksDialog(data.getData()) .show(getFragmentManager(), FRAG_TAG_IMPORT_TASKS); } + } else if (requestCode == RC_DRIVE_BACKUP) { + ((CheckBoxPreference) findPreference(R.string.p_google_drive_backup)) + .setChecked(resultCode == RESULT_OK); } else { super.onActivityResult(requestCode, resultCode, data); } diff --git a/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java b/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java index a1a65e915..5ec3c63cc 100644 --- a/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java +++ b/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java @@ -111,8 +111,7 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity { } private void requestLogin() { - startActivityForResult( - new Intent(SynchronizationPreferences.this, GtasksLoginActivity.class), REQUEST_LOGIN); + startActivityForResult(new Intent(this, GtasksLoginActivity.class), REQUEST_LOGIN); } @Override diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 65c43ff73..df4cbf794 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -19,6 +19,8 @@ custom_files_dir p_backup_dir + p_google_drive_backup + p_google_drive_backup_account notif_enabled enable_qhours diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd4116f00..a32897182 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -656,6 +656,7 @@ File %1$s contained %2$s.\n\n Quiet hours Attachment directory Backup directory + Copy to Google Drive Miscellaneous Synchronization Enabled diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6345add54..1701b29fd 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -72,6 +72,11 @@ android:key="@string/p_backup_dir" android:title="@string/backup_directory"/> + +