mirror of https://github.com/tasks/tasks
Convert GoogleTaskSynchronizer to Kotlin
parent
0146cd5766
commit
90571eca35
@ -1,498 +0,0 @@
|
||||
package org.tasks.gtasks;
|
||||
|
||||
import static com.google.common.collect.Lists.transform;
|
||||
import static org.tasks.Strings.isNullOrEmpty;
|
||||
import static org.tasks.date.DateTimeUtils.newDateTime;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
|
||||
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.todoroo.andlib.utility.DateUtilities;
|
||||
import com.todoroo.astrid.api.Filter;
|
||||
import com.todoroo.astrid.api.GtasksFilter;
|
||||
import com.todoroo.astrid.dao.TaskDaoBlocking;
|
||||
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 dagger.hilt.android.qualifiers.ApplicationContext;
|
||||
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.Firebase;
|
||||
import org.tasks.billing.Inventory;
|
||||
import org.tasks.data.GoogleTask;
|
||||
import org.tasks.data.GoogleTaskAccount;
|
||||
import org.tasks.data.GoogleTaskDaoBlocking;
|
||||
import org.tasks.data.GoogleTaskList;
|
||||
import org.tasks.data.GoogleTaskListDaoBlocking;
|
||||
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 (isNullOrEmpty(o1.getParent())) {
|
||||
return isNullOrEmpty(o2.getParent()) ? 0 : -1;
|
||||
} else {
|
||||
return isNullOrEmpty(o2.getParent()) ? 1 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
private final GoogleTaskListDaoBlocking googleTaskListDao;
|
||||
private final GtasksListService gtasksListService;
|
||||
private final Preferences preferences;
|
||||
private final TaskDaoBlocking taskDao;
|
||||
private final Firebase firebase;
|
||||
private final GoogleTaskDaoBlocking 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(
|
||||
@ApplicationContext Context context,
|
||||
GoogleTaskListDaoBlocking googleTaskListDao,
|
||||
GtasksListService gtasksListService,
|
||||
Preferences preferences,
|
||||
TaskDaoBlocking taskDao,
|
||||
Firebase firebase,
|
||||
GoogleTaskDaoBlocking 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.firebase = firebase;
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
firebase.reportException(e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
account.setError(e.getMessage());
|
||||
firebase.reportException(e);
|
||||
} finally {
|
||||
googleTaskListDao.update(account);
|
||||
localBroadcastManager.broadcastRefreshList();
|
||||
Timber.d("%s: end sync", account);
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronize(GoogleTaskAccount account) throws IOException {
|
||||
if (!permissionChecker.canAccessAccounts()
|
||||
|| googleAccountManager.getAccount(account.getAccount()) == null) {
|
||||
account.setError(context.getString(R.string.cannot_access_account));
|
||||
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 (!isNullOrEmpty(nextPageToken));
|
||||
gtasksListService.updateLists(account, gtaskLists);
|
||||
Filter defaultRemoteList = defaultFilterProvider.getDefaultList();
|
||||
if (defaultRemoteList instanceof GtasksFilter) {
|
||||
GoogleTaskList list =
|
||||
googleTaskListDao.getByRemoteId(((GtasksFilter) defaultRemoteList).getRemoteId());
|
||||
if (list == null) {
|
||||
preferences.setString(R.string.p_default_list, null);
|
||||
}
|
||||
}
|
||||
for (GoogleTaskList list :
|
||||
googleTaskListDao.getByRemoteId(transform(gtaskLists, TaskList::getId))) {
|
||||
if (isNullOrEmpty(list.getRemoteId())) {
|
||||
firebase.reportException(new RuntimeException("Empty remote id"));
|
||||
continue;
|
||||
}
|
||||
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 (!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.getDefaultList();
|
||||
String listId =
|
||||
defaultRemoteList instanceof GtasksFilter
|
||||
? ((GtasksFilter) defaultRemoteList).getRemoteId()
|
||||
: DEFAULT_LIST;
|
||||
|
||||
if (isNullOrEmpty(gtasksMetadata.getRemoteId())) { // Create case
|
||||
String selectedList = gtasksMetadata.getListId();
|
||||
if (!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 && (isNullOrEmpty(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, 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,
|
||||
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(
|
||||
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.suppressSync();
|
||||
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;
|
||||
try {
|
||||
taskList =
|
||||
gtasksInvoker.getAllGtasksFromListId(listId, lastSyncDate + 1000L, nextPageToken);
|
||||
} catch (HttpNotFoundException e) {
|
||||
firebase.reportException(e);
|
||||
return;
|
||||
}
|
||||
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 (!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.fetchBlocking(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(
|
||||
isNullOrEmpty(gtask.getParent())
|
||||
? 0
|
||||
: googleTaskDao.getTask(gtask.getParent()));
|
||||
googleTask.setRemoteId(gtask.getId());
|
||||
}
|
||||
|
||||
if (task == null) {
|
||||
task = taskCreator.createWithValues("");
|
||||
}
|
||||
|
||||
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 isNullOrEmpty(newValue)
|
||||
|| newValue.length() < maxLength
|
||||
|| isNullOrEmpty(currentValue)
|
||||
|| !currentValue.startsWith(newValue)
|
||||
? newValue
|
||||
: currentValue;
|
||||
}
|
||||
|
||||
private void write(Task task, GoogleTask googleTask) {
|
||||
if (!(isNullOrEmpty(task.getTitle()) && isNullOrEmpty(task.getNotes()))) {
|
||||
task.suppressSync();
|
||||
task.suppressRefresh();
|
||||
if (task.isNew()) {
|
||||
taskDao.createNew(task);
|
||||
}
|
||||
taskDao.save(task);
|
||||
googleTask.setTask(task.getId());
|
||||
if (googleTask.getId() == 0) {
|
||||
googleTaskDao.insert(googleTask);
|
||||
} else {
|
||||
googleTaskDao.update(googleTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,414 @@
|
||||
package org.tasks.gtasks
|
||||
|
||||
import android.content.Context
|
||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
||||
import com.google.api.services.tasks.model.Task
|
||||
import com.google.api.services.tasks.model.TaskList
|
||||
import com.google.api.services.tasks.model.Tasks
|
||||
import com.google.common.collect.Lists
|
||||
import com.todoroo.andlib.utility.DateUtilities
|
||||
import com.todoroo.astrid.api.GtasksFilter
|
||||
import com.todoroo.astrid.dao.TaskDaoBlocking
|
||||
import com.todoroo.astrid.data.Task.Companion.createDueDate
|
||||
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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.tasks.LocalBroadcastManager
|
||||
import org.tasks.R
|
||||
import org.tasks.Strings.isNullOrEmpty
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.data.*
|
||||
import org.tasks.date.DateTimeUtils.newDateTime
|
||||
import org.tasks.preferences.DefaultFilterProvider
|
||||
import org.tasks.preferences.PermissionChecker
|
||||
import org.tasks.preferences.Preferences
|
||||
import timber.log.Timber
|
||||
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.*
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLException
|
||||
import kotlin.math.max
|
||||
|
||||
class GoogleTaskSynchronizer @Inject constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val googleTaskListDao: GoogleTaskListDaoBlocking,
|
||||
private val gtasksListService: GtasksListService,
|
||||
private val preferences: Preferences,
|
||||
private val taskDao: TaskDaoBlocking,
|
||||
private val firebase: Firebase,
|
||||
private val googleTaskDao: GoogleTaskDaoBlocking,
|
||||
private val taskCreator: TaskCreator,
|
||||
private val defaultFilterProvider: DefaultFilterProvider,
|
||||
private val permissionChecker: PermissionChecker,
|
||||
private val googleAccountManager: GoogleAccountManager,
|
||||
private val localBroadcastManager: LocalBroadcastManager,
|
||||
private val inventory: Inventory,
|
||||
private val taskDeleter: TaskDeleter,
|
||||
private val gtasksInvoker: GtasksInvoker) {
|
||||
fun sync(account: GoogleTaskAccount, i: Int) {
|
||||
Timber.d("%s: start sync", account)
|
||||
try {
|
||||
if (i == 0 || inventory.hasPro()) {
|
||||
synchronize(account)
|
||||
} else {
|
||||
account.error = context.getString(R.string.requires_pro_subscription)
|
||||
}
|
||||
} catch (e: SocketTimeoutException) {
|
||||
Timber.e(e)
|
||||
account.error = e.message
|
||||
} catch (e: SSLException) {
|
||||
Timber.e(e)
|
||||
account.error = e.message
|
||||
} catch (e: SocketException) {
|
||||
Timber.e(e)
|
||||
account.error = e.message
|
||||
} catch (e: UnknownHostException) {
|
||||
Timber.e(e)
|
||||
account.error = e.message
|
||||
} catch (e: HttpRetryException) {
|
||||
Timber.e(e)
|
||||
account.error = e.message
|
||||
} catch (e: EOFException) {
|
||||
Timber.e(e)
|
||||
account.error = e.message
|
||||
} catch (e: GoogleJsonResponseException) {
|
||||
account.error = e.message
|
||||
if (e.statusCode == 401) {
|
||||
Timber.e(e)
|
||||
} else {
|
||||
firebase.reportException(e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
account.error = e.message
|
||||
firebase.reportException(e)
|
||||
} finally {
|
||||
googleTaskListDao.update(account)
|
||||
localBroadcastManager.broadcastRefreshList()
|
||||
Timber.d("%s: end sync", account)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun synchronize(account: GoogleTaskAccount) {
|
||||
if (!permissionChecker.canAccessAccounts()
|
||||
|| googleAccountManager.getAccount(account.account) == null) {
|
||||
account.error = context.getString(R.string.cannot_access_account)
|
||||
return
|
||||
}
|
||||
val gtasksInvoker = gtasksInvoker.forAccount(account.account)
|
||||
pushLocalChanges(account, gtasksInvoker)
|
||||
val gtaskLists: MutableList<TaskList> = ArrayList()
|
||||
var nextPageToken: String? = null
|
||||
var eTag: String? = null
|
||||
do {
|
||||
val remoteLists = gtasksInvoker.allGtaskLists(nextPageToken) ?: break
|
||||
eTag = remoteLists.etag
|
||||
val items = remoteLists.items
|
||||
if (items != null) {
|
||||
gtaskLists.addAll(items)
|
||||
}
|
||||
nextPageToken = remoteLists.nextPageToken
|
||||
} while (!isNullOrEmpty(nextPageToken))
|
||||
gtasksListService.updateLists(account, gtaskLists)
|
||||
val defaultRemoteList = defaultFilterProvider.defaultList
|
||||
if (defaultRemoteList is GtasksFilter) {
|
||||
val list = googleTaskListDao.getByRemoteId(defaultRemoteList.remoteId)
|
||||
if (list == null) {
|
||||
preferences.setString(R.string.p_default_list, null)
|
||||
}
|
||||
}
|
||||
for (list in googleTaskListDao.getByRemoteId(Lists.transform(gtaskLists) { obj: TaskList? -> obj!!.id })) {
|
||||
if (isNullOrEmpty(list.remoteId)) {
|
||||
firebase.reportException(RuntimeException("Empty remote id"))
|
||||
continue
|
||||
}
|
||||
fetchAndApplyRemoteChanges(gtasksInvoker, list)
|
||||
if (!preferences.isPositionHackEnabled) {
|
||||
googleTaskDao.reposition(list.remoteId!!)
|
||||
}
|
||||
}
|
||||
if (preferences.isPositionHackEnabled) {
|
||||
for (list in gtaskLists) {
|
||||
val tasks = fetchPositions(gtasksInvoker, list.id)
|
||||
for (task in tasks) {
|
||||
googleTaskDao.updatePosition(task.id, task.parent, task.position)
|
||||
}
|
||||
googleTaskDao.reposition(list.id)
|
||||
}
|
||||
}
|
||||
account.etag = eTag
|
||||
account.error = ""
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun fetchPositions(
|
||||
gtasksInvoker: GtasksInvoker, listId: String): List<Task> {
|
||||
val tasks: MutableList<Task> = ArrayList()
|
||||
var nextPageToken: String? = null
|
||||
do {
|
||||
val taskList = gtasksInvoker.getAllPositions(listId, nextPageToken) ?: break
|
||||
val items = taskList.items
|
||||
if (items != null) {
|
||||
tasks.addAll(items)
|
||||
}
|
||||
nextPageToken = taskList.nextPageToken
|
||||
} while (!isNullOrEmpty(nextPageToken))
|
||||
return tasks
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun pushLocalChanges(account: GoogleTaskAccount, gtasksInvoker: GtasksInvoker) {
|
||||
val tasks = taskDao.getGoogleTasksToPush(account.account!!)
|
||||
for (task in tasks) {
|
||||
pushTask(task, gtasksInvoker)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun pushTask(task: com.todoroo.astrid.data.Task, gtasksInvoker: GtasksInvoker) {
|
||||
for (deleted in googleTaskDao.getDeletedByTaskId(task.id)) {
|
||||
gtasksInvoker.deleteGtask(deleted.listId, deleted.remoteId)
|
||||
googleTaskDao.delete(deleted)
|
||||
}
|
||||
val gtasksMetadata = googleTaskDao.getByTaskId(task.id) ?: return
|
||||
val remoteModel = Task()
|
||||
var newlyCreated = false
|
||||
val remoteId: String?
|
||||
val defaultRemoteList = defaultFilterProvider.defaultList
|
||||
var listId = if (defaultRemoteList is GtasksFilter) defaultRemoteList.remoteId else DEFAULT_LIST
|
||||
if (isNullOrEmpty(gtasksMetadata.remoteId)) { // Create case
|
||||
val selectedList = gtasksMetadata.listId
|
||||
if (!isNullOrEmpty(selectedList)) {
|
||||
listId = selectedList
|
||||
}
|
||||
newlyCreated = true
|
||||
} else { // update case
|
||||
remoteId = gtasksMetadata.remoteId
|
||||
listId = gtasksMetadata.listId
|
||||
remoteModel.id = 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 && (isNullOrEmpty(task.title) || task.deletionDate > 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the remote model's changed properties
|
||||
if (task.isDeleted) {
|
||||
remoteModel.deleted = true
|
||||
}
|
||||
remoteModel.title = truncate(task.title, MAX_TITLE_LENGTH)
|
||||
remoteModel.notes = truncate(task.notes, MAX_DESCRIPTION_LENGTH)
|
||||
if (task.hasDueDate()) {
|
||||
remoteModel.due = GtasksApiUtilities.unixTimeToGtasksDueDate(task.dueDate)
|
||||
}
|
||||
if (task.isCompleted) {
|
||||
remoteModel.completed = GtasksApiUtilities.unixTimeToGtasksCompletionTime(task.completionDate)
|
||||
remoteModel.status = "completed" // $NON-NLS-1$
|
||||
} else {
|
||||
remoteModel.completed = null
|
||||
remoteModel.status = "needsAction" // $NON-NLS-1$
|
||||
}
|
||||
if (newlyCreated) {
|
||||
val parent = gtasksMetadata.parent
|
||||
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null
|
||||
val previous = googleTaskDao.getPrevious(
|
||||
listId!!, if (isNullOrEmpty(localParent)) 0 else parent, gtasksMetadata.order)
|
||||
val created: Task?
|
||||
created = try {
|
||||
gtasksInvoker.createGtask(listId, remoteModel, localParent, previous)
|
||||
} catch (e: HttpNotFoundException) {
|
||||
gtasksInvoker.createGtask(listId, remoteModel, null, null)
|
||||
}
|
||||
if (created != null) {
|
||||
// Update the metadata for the newly created task
|
||||
gtasksMetadata.remoteId = created.id
|
||||
gtasksMetadata.listId = listId
|
||||
gtasksMetadata.remoteOrder = created.position.toLong()
|
||||
gtasksMetadata.remoteParent = created.parent
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (!task.isDeleted && gtasksMetadata.isMoved) {
|
||||
try {
|
||||
val parent = gtasksMetadata.parent
|
||||
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null
|
||||
val previous = googleTaskDao.getPrevious(
|
||||
listId!!,
|
||||
if (isNullOrEmpty(localParent)) 0 else parent,
|
||||
gtasksMetadata.order)
|
||||
val result = gtasksInvoker.moveGtask(listId, remoteModel.id, localParent, previous)
|
||||
gtasksMetadata.remoteOrder = result!!.position.toLong()
|
||||
gtasksMetadata.remoteParent = result.parent
|
||||
gtasksMetadata.parent = if (isNullOrEmpty(result.parent)) 0 else googleTaskDao.getTask(result.parent)
|
||||
} catch (e: GoogleJsonResponseException) {
|
||||
if (e.statusCode == 400) {
|
||||
Timber.e(e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: don't updateGtask if it was only moved
|
||||
gtasksInvoker.updateGtask(listId, remoteModel)
|
||||
} catch (e: HttpNotFoundException) {
|
||||
googleTaskDao.delete(gtasksMetadata)
|
||||
return
|
||||
}
|
||||
}
|
||||
task.modificationDate = DateUtilities.now()
|
||||
gtasksMetadata.isMoved = false
|
||||
gtasksMetadata.lastSync = DateUtilities.now() + 1000L
|
||||
if (gtasksMetadata.id == com.todoroo.astrid.data.Task.NO_ID) {
|
||||
googleTaskDao.insert(gtasksMetadata)
|
||||
} else {
|
||||
googleTaskDao.update(gtasksMetadata)
|
||||
}
|
||||
task.suppressSync()
|
||||
taskDao.save(task)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
private fun fetchAndApplyRemoteChanges(
|
||||
gtasksInvoker: GtasksInvoker, list: GoogleTaskList) {
|
||||
val listId = list.remoteId
|
||||
var lastSyncDate = list.lastSync
|
||||
val tasks: MutableList<Task> = ArrayList()
|
||||
var nextPageToken: String? = null
|
||||
do {
|
||||
val taskList: Tasks = try {
|
||||
gtasksInvoker.getAllGtasksFromListId(listId, lastSyncDate + 1000L, nextPageToken)
|
||||
} catch (e: HttpNotFoundException) {
|
||||
firebase.reportException(e)
|
||||
return
|
||||
} ?: break
|
||||
|
||||
val items = taskList.items
|
||||
if (items != null) {
|
||||
tasks.addAll(items)
|
||||
}
|
||||
nextPageToken = taskList.nextPageToken
|
||||
} while (!isNullOrEmpty(nextPageToken))
|
||||
Collections.sort(tasks, PARENTS_FIRST)
|
||||
for (gtask in tasks) {
|
||||
val remoteId = gtask.id
|
||||
var googleTask = googleTaskDao.getByRemoteId(remoteId)
|
||||
var task: com.todoroo.astrid.data.Task? = null
|
||||
if (googleTask == null) {
|
||||
googleTask = GoogleTask(0, "")
|
||||
} else if (googleTask.task > 0) {
|
||||
task = taskDao.fetchBlocking(googleTask.task)
|
||||
}
|
||||
val updated = gtask.updated
|
||||
if (updated != null) {
|
||||
lastSyncDate = max(lastSyncDate, updated.value)
|
||||
}
|
||||
val isDeleted = gtask.deleted
|
||||
val isHidden = gtask.hidden
|
||||
if (isDeleted != null && isDeleted) {
|
||||
if (task != null) {
|
||||
taskDeleter.delete(task)
|
||||
}
|
||||
continue
|
||||
} else if (isHidden != null && isHidden) {
|
||||
if (task == null) {
|
||||
continue
|
||||
}
|
||||
if (task.isRecurring) {
|
||||
googleTask.remoteId = ""
|
||||
} else {
|
||||
taskDeleter.delete(task)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
googleTask.remoteOrder = gtask.position.toLong()
|
||||
googleTask.remoteParent = gtask.parent
|
||||
googleTask.parent = if (isNullOrEmpty(gtask.parent)) 0 else googleTaskDao.getTask(gtask.parent)
|
||||
googleTask.remoteId = gtask.id
|
||||
}
|
||||
if (task == null) {
|
||||
task = taskCreator.createWithValues("")
|
||||
}
|
||||
task!!.title = getTruncatedValue(task.title, gtask.title, MAX_TITLE_LENGTH)
|
||||
task.creationDate = DateUtilities.now()
|
||||
task.completionDate = GtasksApiUtilities.gtasksCompletedTimeToUnixTime(gtask.completed)
|
||||
val dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(gtask.due)
|
||||
mergeDates(createDueDate(com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY, dueDate), task)
|
||||
task.notes = getTruncatedValue(task.notes, gtask.notes, MAX_DESCRIPTION_LENGTH)
|
||||
googleTask.listId = listId
|
||||
googleTask.lastSync = DateUtilities.now() + 1000L
|
||||
write(task, googleTask)
|
||||
}
|
||||
list.lastSync = lastSyncDate
|
||||
googleTaskListDao.insertOrReplace(list)
|
||||
}
|
||||
|
||||
private fun write(task: com.todoroo.astrid.data.Task?, googleTask: GoogleTask) {
|
||||
if (!(isNullOrEmpty(task!!.title) && isNullOrEmpty(task.notes))) {
|
||||
task.suppressSync()
|
||||
task.suppressRefresh()
|
||||
if (task.isNew) {
|
||||
taskDao.createNew(task)
|
||||
}
|
||||
taskDao.save(task)
|
||||
googleTask.task = task.id
|
||||
if (googleTask.id == 0L) {
|
||||
googleTaskDao.insert(googleTask)
|
||||
} else {
|
||||
googleTaskDao.update(googleTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_LIST = "@default" // $NON-NLS-1$
|
||||
private const val MAX_TITLE_LENGTH = 1024
|
||||
private const val MAX_DESCRIPTION_LENGTH = 8192
|
||||
private val PARENTS_FIRST = Comparator { o1: Task, o2: Task ->
|
||||
if (isNullOrEmpty(o1.parent)) {
|
||||
if (isNullOrEmpty(o2.parent)) 0 else -1
|
||||
} else {
|
||||
if (isNullOrEmpty(o2.parent)) 1 else 0
|
||||
}
|
||||
}
|
||||
|
||||
fun mergeDates(remoteDueDate: Long, local: com.todoroo.astrid.data.Task?) {
|
||||
if (remoteDueDate > 0 && local!!.hasDueTime()) {
|
||||
val oldDate = newDateTime(local.dueDate)
|
||||
val newDate = newDateTime(remoteDueDate)
|
||||
.withHourOfDay(oldDate.hourOfDay)
|
||||
.withMinuteOfHour(oldDate.minuteOfHour)
|
||||
.withSecondOfMinute(oldDate.secondOfMinute)
|
||||
local.setDueDateAdjustingHideUntil(
|
||||
createDueDate(com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY_TIME, newDate.millis))
|
||||
} else {
|
||||
local!!.setDueDateAdjustingHideUntil(remoteDueDate)
|
||||
}
|
||||
}
|
||||
|
||||
fun truncate(string: String?, max: Int): String? {
|
||||
return if (string == null || string.length <= max) string else string.substring(0, max)
|
||||
}
|
||||
|
||||
fun getTruncatedValue(currentValue: String?, newValue: String?, maxLength: Int): String? {
|
||||
return if (isNullOrEmpty(newValue)
|
||||
|| newValue!!.length < maxLength || isNullOrEmpty(currentValue)
|
||||
|| !currentValue!!.startsWith(newValue)) newValue else currentValue
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue