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

424 lines
16 KiB
Java

package org.tasks.gtasks;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.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;
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.GtasksTaskListUpdater;
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.gtasks.sync.GtasksSyncService;
import com.todoroo.astrid.gtasks.sync.GtasksTaskContainer;
import com.todoroo.astrid.service.TaskCreator;
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;
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 final Context context;
private final GoogleTaskListDao googleTaskListDao;
private final GtasksSyncService gtasksSyncService;
private final GtasksListService gtasksListService;
private final GtasksTaskListUpdater gtasksTaskListUpdater;
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 PlayServices playServices;
private final PermissionChecker permissionChecker;
private final GoogleAccountManager googleAccountManager;
private final LocalBroadcastManager localBroadcastManager;
@Inject
public GoogleTaskSynchronizer(
@ForApplication Context context,
GoogleTaskListDao googleTaskListDao,
GtasksSyncService gtasksSyncService,
GtasksListService gtasksListService,
GtasksTaskListUpdater gtasksTaskListUpdater,
Preferences preferences,
TaskDao taskDao,
Tracker tracker,
NotificationManager notificationManager,
GoogleTaskDao googleTaskDao,
TaskCreator taskCreator,
DefaultFilterProvider defaultFilterProvider,
PlayServices playServices,
PermissionChecker permissionChecker,
GoogleAccountManager googleAccountManager,
LocalBroadcastManager localBroadcastManager) {
this.context = context;
this.googleTaskListDao = googleTaskListDao;
this.gtasksSyncService = gtasksSyncService;
this.gtasksListService = gtasksListService;
this.gtasksTaskListUpdater = gtasksTaskListUpdater;
this.preferences = preferences;
this.taskDao = taskDao;
this.tracker = tracker;
this.notificationManager = notificationManager;
this.googleTaskDao = googleTaskDao;
this.taskCreator = taskCreator;
this.defaultFilterProvider = defaultFilterProvider;
this.playServices = playServices;
this.permissionChecker = permissionChecker;
this.googleAccountManager = googleAccountManager;
this.localBroadcastManager = localBroadcastManager;
}
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() {
for (GoogleTaskAccount account : googleTaskListDao.getAccounts()) {
Timber.d("%s: start sync", account);
try {
synchronize(account);
account.setError("");
} catch (UserRecoverableAuthIOException e) {
Timber.e(e);
sendNotification(context, e.getIntent());
} catch (IOException e) {
account.setError(e.getMessage());
Timber.e(e);
} 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_DEFAULT)
.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 = new GtasksInvoker(context, playServices, account.getAccount());
pushLocalChanges(gtasksInvoker);
List<TaskList> gtaskLists = new ArrayList<>();
String nextPageToken = null;
do {
TaskLists remoteLists = gtasksInvoker.allGtaskLists(nextPageToken);
if (remoteLists == null) {
break;
}
List<TaskList> items = remoteLists.getItems();
if (items != null) {
gtaskLists.addAll(items);
}
nextPageToken = remoteLists.getNextPageToken();
} while (nextPageToken != null);
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 (final GoogleTaskList list : gtasksListService.getListsToUpdate(gtaskLists)) {
fetchAndApplyRemoteChanges(gtasksInvoker, list);
}
}
private void pushLocalChanges(GtasksInvoker gtasksInvoker) throws UserRecoverableAuthIOException {
List<Task> tasks = taskDao.getGoogleTasksToPush();
for (Task task : tasks) {
try {
pushTask(task, gtasksInvoker);
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
Timber.e(e);
}
}
}
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;
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;
}
remoteModel = new com.google.api.services.tasks.model.Task();
newlyCreated = true;
} else { // update case
remoteId = gtasksMetadata.getRemoteId();
listId = gtasksMetadata.getListId();
remoteModel = new com.google.api.services.tasks.model.Task();
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(task.getTitle());
remoteModel.setNotes(task.getNotes());
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) {
try {
gtasksInvoker.updateGtask(listId, remoteModel);
} catch (HttpNotFoundException e) {
Timber.e(e);
googleTaskDao.delete(gtasksMetadata);
return;
}
} else {
String parent = gtasksSyncService.getRemoteParentId(gtasksMetadata);
String priorSibling = gtasksSyncService.getRemoteSiblingId(listId, gtasksMetadata);
com.google.api.services.tasks.model.Task created =
gtasksInvoker.createGtask(listId, remoteModel, parent, priorSibling);
if (created != null) {
// Update the metadata for the newly created task
gtasksMetadata.setRemoteId(created.getId());
gtasksMetadata.setListId(listId);
} else {
return;
}
}
task.setModificationDate(DateUtilities.now());
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 UserRecoverableAuthIOException {
String listId = list.getRemoteId();
long lastSyncDate = list.getLastSync();
boolean includeDeletedAndHidden = lastSyncDate != 0;
try {
List<com.google.api.services.tasks.model.Task> tasks = new ArrayList<>();
String nextPageToken = null;
do {
Tasks taskList =
gtasksInvoker.getAllGtasksFromListId(
listId,
includeDeletedAndHidden,
includeDeletedAndHidden,
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 (nextPageToken != null);
if (!tasks.isEmpty()) {
for (com.google.api.services.tasks.model.Task gtask : tasks) {
String remoteId = gtask.getId();
GoogleTask googleTask = getMetadataByGtaskId(remoteId);
Task task = null;
if (googleTask == null) {
googleTask = new GoogleTask(0, "");
} else if (googleTask.getTask() > 0) {
task = taskDao.fetch(googleTask.getTask());
}
if (task == null) {
task = taskCreator.createWithValues(null, "");
}
GtasksTaskContainer container = new GtasksTaskContainer(gtask, task, listId, googleTask);
container.gtaskMetadata.setRemoteOrder(Long.parseLong(gtask.getPosition()));
container.gtaskMetadata.setParent(localIdForGtasksId(gtask.getParent()));
container.gtaskMetadata.setLastSync(DateUtilities.now() + 1000L);
write(container);
lastSyncDate = Math.max(lastSyncDate, container.getUpdateTime());
}
list.setLastSync(lastSyncDate);
googleTaskListDao.insertOrReplace(list);
gtasksTaskListUpdater.correctOrderAndIndentForList(listId);
}
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
Timber.e(e);
}
}
private long localIdForGtasksId(String gtasksId) {
GoogleTask metadata = getMetadataByGtaskId(gtasksId);
return metadata == null ? Task.NO_ID : metadata.getTask();
}
private GoogleTask getMetadataByGtaskId(String gtaskId) {
return googleTaskDao.getByRemoteId(gtaskId);
}
private void write(GtasksTaskContainer task) {
if (!TextUtils.isEmpty(task.task.getTitle())) {
task.task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
task.task.putTransitory(TaskDao.TRANS_SUPPRESS_REFRESH, true);
task.prepareForSaving();
if (task.task.isNew()) {
taskDao.createNew(task.task);
}
taskDao.save(task.task);
synchronizeMetadata(task.task.getId(), task.metadata);
}
}
/**
* Synchronize metadata for given task id. Deletes rows in database that are not identical to
* those in the metadata list, creates rows that have no match.
*
* @param taskId id of task to perform synchronization on
* @param metadata list of new metadata items to save
*/
private void synchronizeMetadata(long taskId, ArrayList<GoogleTask> metadata) {
for (GoogleTask metadatum : metadata) {
metadatum.setTask(taskId);
metadatum.setId(0);
}
for (GoogleTask item : googleTaskDao.getAllByTaskId(taskId)) {
long id = item.getId();
// clear item id when matching with incoming values
item.setId(0);
if (metadata.contains(item)) {
metadata.remove(item);
} else {
// not matched. cut it
item.setId(id);
googleTaskDao.delete(item);
}
}
// everything that remains shall be written
for (GoogleTask values : metadata) {
googleTaskDao.insert(values);
}
}
}