You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/app/src/main/java/org/tasks/gtasks/GoogleTaskSynchronizer.java

530 lines
20 KiB
Java

package org.tasks.gtasks;
import static com.google.common.collect.Lists.transform;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.tasks.model.Task.Links;
import com.google.api.services.tasks.model.TaskList;
import com.google.api.services.tasks.model.TaskLists;
import com.google.api.services.tasks.model.Tasks;
import com.google.common.base.Strings;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksListService;
import com.todoroo.astrid.gtasks.api.GtasksApiUtilities;
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.api.HttpNotFoundException;
import com.todoroo.astrid.service.TaskCreator;
import com.todoroo.astrid.service.TaskDeleter;
import com.todoroo.astrid.utility.Constants;
import java.io.EOFException;
import java.io.IOException;
import java.net.HttpRetryException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
import javax.net.ssl.SSLException;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.analytics.Tracker;
import org.tasks.billing.Inventory;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.injection.ForApplication;
import org.tasks.notifications.NotificationManager;
import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import timber.log.Timber;
public class GoogleTaskSynchronizer {
private static final String DEFAULT_LIST = "@default"; // $NON-NLS-1$
private static final int MAX_TITLE_LENGTH = 1024;
private static final int MAX_DESCRIPTION_LENGTH = 8192;
private static final Comparator<com.google.api.services.tasks.model.Task> PARENTS_FIRST =
(o1, o2) -> {
if (Strings.isNullOrEmpty(o1.getParent())) {
return Strings.isNullOrEmpty(o2.getParent()) ? 0 : -1;
} else {
return Strings.isNullOrEmpty(o2.getParent()) ? 1 : 0;
}
};
private final Context context;
private final GoogleTaskListDao googleTaskListDao;
private final GtasksListService gtasksListService;
private final Preferences preferences;
private final TaskDao taskDao;
private final Tracker tracker;
private final NotificationManager notificationManager;
private final GoogleTaskDao googleTaskDao;
private final TaskCreator taskCreator;
private final DefaultFilterProvider defaultFilterProvider;
private final PermissionChecker permissionChecker;
private final GoogleAccountManager googleAccountManager;
private final LocalBroadcastManager localBroadcastManager;
private final Inventory inventory;
private final TaskDeleter taskDeleter;
private final GtasksInvoker gtasksInvoker;
@Inject
public GoogleTaskSynchronizer(
@ForApplication Context context,
GoogleTaskListDao googleTaskListDao,
GtasksListService gtasksListService,
Preferences preferences,
TaskDao taskDao,
Tracker tracker,
NotificationManager notificationManager,
GoogleTaskDao googleTaskDao,
TaskCreator taskCreator,
DefaultFilterProvider defaultFilterProvider,
PermissionChecker permissionChecker,
GoogleAccountManager googleAccountManager,
LocalBroadcastManager localBroadcastManager,
Inventory inventory,
TaskDeleter taskDeleter,
GtasksInvoker gtasksInvoker) {
this.context = context;
this.googleTaskListDao = googleTaskListDao;
this.gtasksListService = gtasksListService;
this.preferences = preferences;
this.taskDao = taskDao;
this.tracker = tracker;
this.notificationManager = notificationManager;
this.googleTaskDao = googleTaskDao;
this.taskCreator = taskCreator;
this.defaultFilterProvider = defaultFilterProvider;
this.permissionChecker = permissionChecker;
this.googleAccountManager = googleAccountManager;
this.localBroadcastManager = localBroadcastManager;
this.inventory = inventory;
this.taskDeleter = taskDeleter;
this.gtasksInvoker = gtasksInvoker;
}
public static void mergeDates(long remoteDueDate, Task local) {
if (remoteDueDate > 0 && local.hasDueTime()) {
DateTime oldDate = newDateTime(local.getDueDate());
DateTime newDate =
newDateTime(remoteDueDate)
.withHourOfDay(oldDate.getHourOfDay())
.withMinuteOfHour(oldDate.getMinuteOfHour())
.withSecondOfMinute(oldDate.getSecondOfMinute());
local.setDueDateAdjustingHideUntil(
Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDate.getMillis()));
} else {
local.setDueDateAdjustingHideUntil(remoteDueDate);
}
}
public void sync(GoogleTaskAccount account, int i) {
Timber.d("%s: start sync", account);
try {
if (i == 0 || inventory.hasPro()) {
synchronize(account);
} else {
account.setError(context.getString(R.string.requires_pro_subscription));
}
} catch (SocketTimeoutException
| SSLException
| SocketException
| UnknownHostException
| HttpRetryException
| EOFException e) {
Timber.e(e);
account.setError(e.getMessage());
} catch (GoogleJsonResponseException e) {
account.setError(e.getMessage());
if (e.getStatusCode() == 401) {
Timber.e(e);
} else {
tracker.reportException(e);
}
} catch (UserRecoverableAuthIOException e) {
Timber.e(e);
sendNotification(context, e.getIntent());
} catch (Exception e) {
account.setError(e.getMessage());
tracker.reportException(e);
} finally {
googleTaskListDao.update(account);
localBroadcastManager.broadcastRefreshList();
Timber.d("%s: end sync", account);
}
}
private void sendNotification(Context context, Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_FROM_BACKGROUND);
PendingIntent resolve =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(
context, NotificationManager.NOTIFICATION_CHANNEL_MISCELLANEOUS)
.setAutoCancel(true)
.setContentIntent(resolve)
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(
context.getString(R.string.common_google_play_services_notification_ticker))
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_warning_white_24dp)
.setTicker(context.getString(R.string.common_google_play_services_notification_ticker));
notificationManager.notify(Constants.NOTIFICATION_SYNC_ERROR, builder, true, false, false);
}
private void synchronize(GoogleTaskAccount account) throws IOException {
if (!permissionChecker.canAccessAccounts()
|| googleAccountManager.getAccount(account.getAccount()) == null) {
account.setError(context.getString(R.string.cannot_access_account));
googleTaskListDao.update(account);
localBroadcastManager.broadcastRefreshList();
return;
}
GtasksInvoker gtasksInvoker = this.gtasksInvoker.forAccount(account.getAccount());
pushLocalChanges(account, gtasksInvoker);
List<TaskList> gtaskLists = new ArrayList<>();
String nextPageToken = null;
String eTag = null;
do {
TaskLists remoteLists = gtasksInvoker.allGtaskLists(nextPageToken);
if (remoteLists == null) {
break;
}
eTag = remoteLists.getEtag();
List<TaskList> items = remoteLists.getItems();
if (items != null) {
gtaskLists.addAll(items);
}
nextPageToken = remoteLists.getNextPageToken();
} while (!Strings.isNullOrEmpty(nextPageToken));
gtasksListService.updateLists(account, gtaskLists);
Filter defaultRemoteList = defaultFilterProvider.getDefaultRemoteList();
if (defaultRemoteList instanceof GtasksFilter) {
GoogleTaskList list =
gtasksListService.getList(((GtasksFilter) defaultRemoteList).getRemoteId());
if (list == null) {
preferences.setString(R.string.p_default_remote_list, null);
}
}
for (GoogleTaskList list :
googleTaskListDao.getByRemoteId(transform(gtaskLists, TaskList::getId))) {
fetchAndApplyRemoteChanges(gtasksInvoker, list);
if (!preferences.isPositionHackEnabled()) {
googleTaskDao.reposition(list.getRemoteId());
}
}
if (preferences.isPositionHackEnabled()) {
for (TaskList list : gtaskLists) {
List<com.google.api.services.tasks.model.Task> tasks =
fetchPositions(gtasksInvoker, list.getId());
for (com.google.api.services.tasks.model.Task task : tasks) {
googleTaskDao.updatePosition(task.getId(), task.getParent(), task.getPosition());
}
googleTaskDao.reposition(list.getId());
}
}
account.setEtag(eTag);
account.setError("");
}
private List<com.google.api.services.tasks.model.Task> fetchPositions(
GtasksInvoker gtasksInvoker, String listId) throws IOException {
List<com.google.api.services.tasks.model.Task> tasks = new ArrayList<>();
String nextPageToken = null;
do {
Tasks taskList = gtasksInvoker.getAllPositions(listId, nextPageToken);
if (taskList == null) {
break;
}
List<com.google.api.services.tasks.model.Task> items = taskList.getItems();
if (items != null) {
tasks.addAll(items);
}
nextPageToken = taskList.getNextPageToken();
} while (!Strings.isNullOrEmpty(nextPageToken));
return tasks;
}
private void pushLocalChanges(GoogleTaskAccount account, GtasksInvoker gtasksInvoker)
throws IOException {
List<Task> tasks = taskDao.getGoogleTasksToPush(account.getAccount());
for (Task task : tasks) {
pushTask(task, gtasksInvoker);
}
}
private void pushTask(Task task, GtasksInvoker gtasksInvoker) throws IOException {
for (GoogleTask deleted : googleTaskDao.getDeletedByTaskId(task.getId())) {
gtasksInvoker.deleteGtask(deleted.getListId(), deleted.getRemoteId());
googleTaskDao.delete(deleted);
}
GoogleTask gtasksMetadata = googleTaskDao.getByTaskId(task.getId());
if (gtasksMetadata == null) {
return;
}
com.google.api.services.tasks.model.Task remoteModel =
new com.google.api.services.tasks.model.Task();
boolean newlyCreated = false;
String remoteId;
Filter defaultRemoteList = defaultFilterProvider.getDefaultRemoteList();
String listId =
defaultRemoteList instanceof GtasksFilter
? ((GtasksFilter) defaultRemoteList).getRemoteId()
: DEFAULT_LIST;
if (Strings.isNullOrEmpty(gtasksMetadata.getRemoteId())) { // Create case
String selectedList = gtasksMetadata.getListId();
if (!Strings.isNullOrEmpty(selectedList)) {
listId = selectedList;
}
newlyCreated = true;
} else { // update case
remoteId = gtasksMetadata.getRemoteId();
listId = gtasksMetadata.getListId();
remoteModel.setId(remoteId);
}
// If task was newly created but without a title, don't sync--we're in the middle of
// creating a task which may end up being cancelled. Also don't sync new but already
// deleted tasks
if (newlyCreated && (TextUtils.isEmpty(task.getTitle()) || task.getDeletionDate() > 0)) {
return;
}
// Update the remote model's changed properties
if (task.isDeleted()) {
remoteModel.setDeleted(true);
}
remoteModel.setTitle(truncate(task.getTitle(), MAX_TITLE_LENGTH));
remoteModel.setNotes(truncate(task.getNotes(), MAX_DESCRIPTION_LENGTH));
if (task.hasDueDate()) {
remoteModel.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(task.getDueDate()));
}
if (task.isCompleted()) {
remoteModel.setCompleted(
GtasksApiUtilities.unixTimeToGtasksCompletionTime(task.getCompletionDate()));
remoteModel.setStatus("completed"); // $NON-NLS-1$
} else {
remoteModel.setCompleted(null);
remoteModel.setStatus("needsAction"); // $NON-NLS-1$
}
if (newlyCreated) {
long parent = gtasksMetadata.getParent();
String localParent = parent > 0 ? googleTaskDao.getRemoteId(parent) : null;
String previous =
googleTaskDao.getPrevious(
listId, Strings.isNullOrEmpty(localParent) ? 0 : parent, gtasksMetadata.getOrder());
com.google.api.services.tasks.model.Task created;
try {
created = gtasksInvoker.createGtask(listId, remoteModel, localParent, previous);
} catch (HttpNotFoundException e) {
created = gtasksInvoker.createGtask(listId, remoteModel, null, null);
}
if (created != null) {
// Update the metadata for the newly created task
gtasksMetadata.setRemoteId(created.getId());
gtasksMetadata.setListId(listId);
gtasksMetadata.setRemoteOrder(Long.parseLong(created.getPosition()));
gtasksMetadata.setRemoteParent(created.getParent());
} else {
return;
}
} else {
try {
if (!task.isDeleted() && gtasksMetadata.isMoved()) {
try {
long parent = gtasksMetadata.getParent();
String localParent = parent > 0 ? googleTaskDao.getRemoteId(parent) : null;
String previous =
googleTaskDao.getPrevious(
listId,
Strings.isNullOrEmpty(localParent) ? 0 : parent,
gtasksMetadata.getOrder());
com.google.api.services.tasks.model.Task result =
gtasksInvoker.moveGtask(listId, remoteModel.getId(), localParent, previous);
gtasksMetadata.setRemoteOrder(Long.parseLong(result.getPosition()));
gtasksMetadata.setRemoteParent(result.getParent());
gtasksMetadata.setParent(
Strings.isNullOrEmpty(result.getParent())
? 0
: googleTaskDao.getTask(result.getParent()));
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == 400) {
Timber.e(e);
} else {
throw e;
}
}
}
// TODO: don't updateGtask if it was only moved
gtasksInvoker.updateGtask(listId, remoteModel);
} catch (HttpNotFoundException e) {
googleTaskDao.delete(gtasksMetadata);
return;
}
}
task.setModificationDate(DateUtilities.now());
gtasksMetadata.setMoved(false);
gtasksMetadata.setLastSync(DateUtilities.now() + 1000L);
if (gtasksMetadata.getId() == Task.NO_ID) {
googleTaskDao.insert(gtasksMetadata);
} else {
googleTaskDao.update(gtasksMetadata);
}
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
taskDao.save(task);
}
private synchronized void fetchAndApplyRemoteChanges(
GtasksInvoker gtasksInvoker, GoogleTaskList list) throws IOException {
String listId = list.getRemoteId();
long lastSyncDate = list.getLastSync();
List<com.google.api.services.tasks.model.Task> tasks = new ArrayList<>();
String nextPageToken = null;
do {
Tasks taskList =
gtasksInvoker.getAllGtasksFromListId(listId, lastSyncDate + 1000L, nextPageToken);
if (taskList == null) {
break;
}
List<com.google.api.services.tasks.model.Task> items = taskList.getItems();
if (items != null) {
tasks.addAll(items);
}
nextPageToken = taskList.getNextPageToken();
} while (!Strings.isNullOrEmpty(nextPageToken));
Collections.sort(tasks, PARENTS_FIRST);
for (com.google.api.services.tasks.model.Task gtask : tasks) {
String remoteId = gtask.getId();
GoogleTask googleTask = googleTaskDao.getByRemoteId(remoteId);
Task task = null;
if (googleTask == null) {
googleTask = new GoogleTask(0, "");
} else if (googleTask.getTask() > 0) {
task = taskDao.fetch(googleTask.getTask());
}
com.google.api.client.util.DateTime updated = gtask.getUpdated();
if (updated != null) {
lastSyncDate = Math.max(lastSyncDate, updated.getValue());
}
Boolean isDeleted = gtask.getDeleted();
Boolean isHidden = gtask.getHidden();
if (isDeleted != null && isDeleted) {
if (task != null) {
taskDeleter.delete(task);
}
continue;
} else if (isHidden != null && isHidden) {
if (task == null) {
continue;
}
if (task.isRecurring()) {
googleTask.setRemoteId("");
} else {
taskDeleter.delete(task);
continue;
}
} else {
googleTask.setRemoteOrder(Long.parseLong(gtask.getPosition()));
googleTask.setRemoteParent(gtask.getParent());
googleTask.setParent(
Strings.isNullOrEmpty(gtask.getParent())
? 0
: googleTaskDao.getTask(gtask.getParent()));
googleTask.setRemoteId(gtask.getId());
}
if (task == null) {
task = taskCreator.createWithValues("");
}
List<Links> links = gtask.getLinks();
if (!links.isEmpty()) {
Links link = links.get(0);
googleTask.setEmailDescription(link.getDescription());
googleTask.setEmailUrl(link.getLink());
}
task.setTitle(getTruncatedValue(task.getTitle(), gtask.getTitle(), MAX_TITLE_LENGTH));
task.setCreationDate(DateUtilities.now());
task.setCompletionDate(
GtasksApiUtilities.gtasksCompletedTimeToUnixTime(gtask.getCompleted()));
long dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(gtask.getDue());
mergeDates(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate), task);
task.setNotes(getTruncatedValue(task.getNotes(), gtask.getNotes(), MAX_DESCRIPTION_LENGTH));
googleTask.setListId(listId);
googleTask.setLastSync(DateUtilities.now() + 1000L);
write(task, googleTask);
}
list.setLastSync(lastSyncDate);
googleTaskListDao.insertOrReplace(list);
}
static String truncate(@Nullable String string, int max) {
return string == null || string.length() <= max ? string : string.substring(0, max);
}
static String getTruncatedValue(@Nullable String currentValue, @Nullable String newValue, int maxLength) {
return Strings.isNullOrEmpty(newValue)
|| newValue.length() < maxLength
|| Strings.isNullOrEmpty(currentValue)
|| !currentValue.startsWith(newValue)
? newValue
: currentValue;
}
private void write(Task task, GoogleTask googleTask) {
if (!(TextUtils.isEmpty(task.getTitle()) && TextUtils.isEmpty(task.getNotes()))) {
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
task.putTransitory(TaskDao.TRANS_SUPPRESS_REFRESH, true);
if (task.isNew()) {
taskDao.createNew(task);
}
taskDao.save(task);
googleTask.setTask(task.getId());
if (googleTask.getId() == 0) {
googleTaskDao.insert(googleTask);
} else {
googleTaskDao.update(googleTask);
}
}
}
}