diff --git a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java index ddbc07388..7308b1be2 100755 --- a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java @@ -7,8 +7,6 @@ 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; @@ -31,7 +29,7 @@ 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.jobs.WorkManager; import org.tasks.preferences.Preferences; import java.io.IOException; @@ -66,7 +64,7 @@ public class TasksJsonExporter { private final GoogleTaskListDao googleTaskListDao; private final TaskAttachmentDao taskAttachmentDao; private final CaldavDao caldavDao; - private final DriveInvoker driveInvoker; + private final WorkManager workManager; private final TaskDao taskDao; private final UserActivityDao userActivityDao; private final Preferences preferences; @@ -90,6 +88,7 @@ public class TasksJsonExporter { GoogleTaskListDao googleTaskListDao, TaskAttachmentDao taskAttachmentDao, CaldavDao caldavDao, + WorkManager workManager, DriveInvoker driveInvoker) { this.tagDataDao = tagDataDao; this.taskDao = taskDao; @@ -103,7 +102,7 @@ public class TasksJsonExporter { this.googleTaskListDao = googleTaskListDao; this.taskAttachmentDao = taskAttachmentDao; this.caldavDao = caldavDao; - this.driveInvoker = driveInvoker; + this.workManager = workManager; } private static String getDateForExport() { @@ -154,13 +153,7 @@ public class TasksJsonExporter { 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); - } + workManager.scheduleDriveUpload(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 index 73280c8bc..16f06b1e2 100644 --- a/app/src/main/java/org/tasks/drive/DriveInvoker.java +++ b/app/src/main/java/org/tasks/drive/DriveInvoker.java @@ -16,6 +16,7 @@ import com.google.api.services.drive.model.File; import org.tasks.BuildConfig; import org.tasks.R; +import org.tasks.files.FileHelper; import org.tasks.injection.ForApplication; import org.tasks.preferences.Preferences; @@ -29,6 +30,8 @@ import timber.log.Timber; public class DriveInvoker { + private static final String MIME_FOLDER = "application/vnd.google-apps.folder"; + private final Drive service; private final Context context; @@ -51,8 +54,19 @@ public class DriveInvoker { } } - public List findFolder(String name) throws IOException { - String query = String.format("name='%s'", name); + public File getFile(String folderId) throws IOException { + return execute(service.files().get(folderId).setFields("id, trashed")); + } + + public void delete(File file) throws IOException { + execute(service.files().delete(file.getId())); + } + + public List getFilesByPrefix(String folderId, String prefix) throws IOException { + String query = + String.format( + "'%s' in parents and name contains '%s' and trashed = false and mimeType != '%s'", + folderId, prefix, MIME_FOLDER); return execute(service.files().list().setQ(query).setSpaces("drive")).getFiles(); } @@ -64,11 +78,12 @@ public class DriveInvoker { return execute(service.files().create(folder).setFields("id")); } - public void createFile(String mime, String parent, String name, Uri uri) throws IOException { + public void createFile(String folderId, Uri uri) throws IOException { + String mime = FileHelper.getMimeType(context, uri); File metadata = new File() - .setParents(Collections.singletonList(parent)) + .setParents(Collections.singletonList(folderId)) .setMimeType(mime) - .setName(name); + .setName(FileHelper.getFilename(context, uri)); InputStreamContent content = new InputStreamContent(mime, context.getContentResolver().openInputStream(uri)); execute(service.files().create(metadata, content)); diff --git a/app/src/main/java/org/tasks/files/FileHelper.java b/app/src/main/java/org/tasks/files/FileHelper.java index ada69f6a0..6b5ff1c81 100644 --- a/app/src/main/java/org/tasks/files/FileHelper.java +++ b/app/src/main/java/org/tasks/files/FileHelper.java @@ -134,15 +134,18 @@ public class FileHelper { return null; } + public static String getMimeType(Context context, Uri uri) { + String filename = getFilename(context, uri); + String extension = Files.getFileExtension(filename); + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + public static void startActionView(Activity context, Uri uri) { if (uri == null) { return; } - MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String filename = getFilename(context, uri); - String extension = Files.getFileExtension(filename); - String mimeType = mimeTypeMap.getMimeTypeFromExtension(extension); + String mimeType = getMimeType(context, uri); Intent intent = new Intent(Intent.ACTION_VIEW); if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { uri = copyToUri(context, Uri.fromFile(context.getCacheDir()), uri); diff --git a/app/src/main/java/org/tasks/injection/JobComponent.java b/app/src/main/java/org/tasks/injection/JobComponent.java index e6d032abc..256df042e 100644 --- a/app/src/main/java/org/tasks/injection/JobComponent.java +++ b/app/src/main/java/org/tasks/injection/JobComponent.java @@ -4,6 +4,7 @@ import dagger.Subcomponent; import org.tasks.jobs.AfterSaveWork; import org.tasks.jobs.BackupWork; import org.tasks.jobs.CleanupWork; +import org.tasks.jobs.DriveUploader; import org.tasks.jobs.MidnightRefreshWork; import org.tasks.jobs.NotificationWork; import org.tasks.jobs.RefreshWork; @@ -25,4 +26,6 @@ public interface JobComponent { void inject(MidnightRefreshWork midnightRefreshWork); void inject(AfterSaveWork afterSaveWork); + + void inject(DriveUploader driveUploader); } diff --git a/app/src/main/java/org/tasks/jobs/BackupWork.java b/app/src/main/java/org/tasks/jobs/BackupWork.java index 509715b0f..833d9be5a 100644 --- a/app/src/main/java/org/tasks/jobs/BackupWork.java +++ b/app/src/main/java/org/tasks/jobs/BackupWork.java @@ -4,15 +4,18 @@ import android.content.Context; import android.net.Uri; import com.google.common.base.Predicate; +import com.google.common.base.Strings; import org.tasks.R; import org.tasks.backup.TasksJsonExporter; +import org.tasks.drive.DriveInvoker; import org.tasks.injection.ForApplication; import org.tasks.injection.JobComponent; import org.tasks.preferences.Preferences; import java.io.File; import java.io.FileFilter; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -40,12 +43,15 @@ public class BackupWork extends RepeatingWorker { (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()); private static final Comparator DOCUMENT_FILE_COMPARATOR = (d1, d2) -> Long.compare(d2.lastModified(), d1.lastModified()); + private static final Comparator DRIVE_FILE_COMPARATOR = + (f1, f2) -> Long.compare(f2.getModifiedTime().getValue(), f1.getModifiedTime().getValue()); private static final int DAYS_TO_KEEP_BACKUP = 7; @Inject @ForApplication Context context; @Inject TasksJsonExporter tasksJsonExporter; @Inject Preferences preferences; @Inject WorkManager workManager; + @Inject DriveInvoker drive; public BackupWork(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); @@ -84,6 +90,11 @@ public class BackupWork extends RepeatingWorker { return newArrayList(skip(files, DAYS_TO_KEEP_BACKUP)); } + private static List getDeleteList(List files) { + Collections.sort(files, DRIVE_FILE_COMPARATOR); + return newArrayList(skip(files, DAYS_TO_KEEP_BACKUP)); + } + @Override protected void inject(JobComponent component) { component.inject(this); @@ -91,7 +102,13 @@ public class BackupWork extends RepeatingWorker { void startBackup(Context context) { try { - deleteOldBackups(); + deleteOldLocalBackups(); + } catch (Exception e) { + Timber.e(e); + } + + try { + deleteOldDriveBackups(); } catch (Exception e) { Timber.e(e); } @@ -104,7 +121,7 @@ public class BackupWork extends RepeatingWorker { } } - private void deleteOldBackups() { + private void deleteOldLocalBackups() { Uri uri = preferences.getBackupDirectory(); switch (uri.getScheme()) { case "content": @@ -126,4 +143,21 @@ public class BackupWork extends RepeatingWorker { break; } } + + private void deleteOldDriveBackups() throws IOException { + if (!preferences.getBoolean(R.string.p_google_drive_backup, false)) { + return; + } + + String folderId = preferences.getStringValue(R.string.p_google_drive_backup_folder); + + if (Strings.isNullOrEmpty(folderId)) { + return; + } + + List files = drive.getFilesByPrefix(folderId, "auto."); + for (com.google.api.services.drive.model.File file : getDeleteList(files)) { + drive.delete(file); + } + } } diff --git a/app/src/main/java/org/tasks/jobs/DriveUploader.java b/app/src/main/java/org/tasks/jobs/DriveUploader.java new file mode 100644 index 000000000..3336caaea --- /dev/null +++ b/app/src/main/java/org/tasks/jobs/DriveUploader.java @@ -0,0 +1,68 @@ +package org.tasks.jobs; + +import android.content.Context; +import android.net.Uri; + +import com.google.api.services.drive.model.File; +import com.google.common.base.Strings; + +import org.tasks.R; +import org.tasks.analytics.Tracker; +import org.tasks.drive.DriveInvoker; +import org.tasks.injection.ForApplication; +import org.tasks.injection.InjectingWorker; +import org.tasks.injection.JobComponent; +import org.tasks.preferences.Preferences; + +import java.io.IOException; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.WorkerParameters; + +public class DriveUploader extends InjectingWorker { + + private static final String FOLDER_NAME = "Tasks Backups"; + private static final String EXTRA_URI = "extra_uri"; + + @Inject @ForApplication Context context; + @Inject DriveInvoker drive; + @Inject Preferences preferences; + @Inject Tracker tracker; + + static Data getInputData(Uri uri) { + return new Data.Builder().putString(EXTRA_URI, uri.toString()).build(); + } + + public DriveUploader(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @Override + protected Result run() { + Data inputData = getInputData(); + Uri uri = Uri.parse(inputData.getString(EXTRA_URI)); + try { + File folder = getFolder(); + preferences.setString(R.string.p_google_drive_backup_folder, folder.getId()); + drive.createFile(folder.getId(), uri); + return Result.SUCCESS; + } catch (IOException e) { + tracker.reportException(e); + return Result.FAILURE; + } + } + + private File getFolder() throws IOException { + String folderId = preferences.getStringValue(R.string.p_google_drive_backup_folder); + File file = Strings.isNullOrEmpty(folderId) ? null : drive.getFile(folderId); + return file == null || file.getTrashed() ? drive.createFolder(FOLDER_NAME) : file; + } + + @Override + protected void inject(JobComponent component) { + component.inject(this); + } +} diff --git a/app/src/main/java/org/tasks/jobs/WorkManager.java b/app/src/main/java/org/tasks/jobs/WorkManager.java index 929ca627d..5a6e495a8 100644 --- a/app/src/main/java/org/tasks/jobs/WorkManager.java +++ b/app/src/main/java/org/tasks/jobs/WorkManager.java @@ -1,5 +1,7 @@ package org.tasks.jobs; +import android.net.Uri; + import static com.todoroo.andlib.utility.DateUtilities.now; import static org.tasks.date.DateTimeUtils.midnight; import static org.tasks.date.DateTimeUtils.newDateTime; @@ -119,11 +121,7 @@ public class WorkManager { ExistingPeriodicWorkPolicy.KEEP, new PeriodicWorkRequest.Builder(SyncWork.class, 1, TimeUnit.HOURS) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setConstraints( - new Constraints.Builder() - .setRequiredNetworkType( - onlyOnUnmetered ? NetworkType.UNMETERED : NetworkType.CONNECTED) - .build()) + .setConstraints(getNetworkConstraints(onlyOnUnmetered)) .build()); } else { workManager.cancelUniqueWork(TAG_BACKGROUND_SYNC); @@ -151,6 +149,28 @@ public class WorkManager { Math.min(newDateTime(lastBackup).plusDays(1).getMillis(), midnight())); } + public void scheduleDriveUpload(Uri uri) { + if (!preferences.getBoolean(R.string.p_google_drive_backup, false)) { + return; + } + + workManager.enqueue( + new Builder(DriveUploader.class) + .setInputData(DriveUploader.getInputData(uri)) + .setConstraints(getNetworkConstraints()) + .build()); + } + + private Constraints getNetworkConstraints() { + return getNetworkConstraints(preferences.getBoolean(R.string.p_background_sync_unmetered_only, false)); + } + + private Constraints getNetworkConstraints(boolean unmeteredOnly) { + return new Constraints.Builder() + .setRequiredNetworkType(unmeteredOnly ? NetworkType.UNMETERED : NetworkType.CONNECTED) + .build(); + } + private void enqueueUnique(String key, Class c, long time) { long delay = time - now(); OneTimeWorkRequest.Builder builder = new Builder(c); diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index df4cbf794..1f55b7dc0 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -21,6 +21,7 @@ p_backup_dir p_google_drive_backup p_google_drive_backup_account + p_google_drive_backup_folder notif_enabled enable_qhours