mirror of https://github.com/tasks/tasks
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.
476 lines
20 KiB
Java
476 lines
20 KiB
Java
/*
|
|
* Copyright 2013 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package org.tasks.gtasks;
|
|
|
|
import android.accounts.Account;
|
|
import android.app.PendingIntent;
|
|
import android.content.ContentProviderClient;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SyncResult;
|
|
import android.os.Bundle;
|
|
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.todoroo.andlib.data.AbstractModel;
|
|
import com.todoroo.andlib.sql.Criterion;
|
|
import com.todoroo.andlib.sql.Join;
|
|
import com.todoroo.andlib.sql.Query;
|
|
import com.todoroo.andlib.utility.DateUtilities;
|
|
import com.todoroo.astrid.dao.MetadataDao;
|
|
import com.todoroo.astrid.dao.StoreObjectDao;
|
|
import com.todoroo.astrid.dao.TaskDao;
|
|
import com.todoroo.astrid.data.Metadata;
|
|
import com.todoroo.astrid.data.SyncFlags;
|
|
import com.todoroo.astrid.data.Task;
|
|
import com.todoroo.astrid.gtasks.GtasksList;
|
|
import com.todoroo.astrid.gtasks.GtasksListService;
|
|
import com.todoroo.astrid.gtasks.GtasksMetadata;
|
|
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
|
|
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.utility.Constants;
|
|
|
|
import org.tasks.LocalBroadcastManager;
|
|
import org.tasks.R;
|
|
import org.tasks.analytics.Tracker;
|
|
import org.tasks.injection.InjectingAbstractThreadedSyncAdapter;
|
|
import org.tasks.injection.SyncAdapterComponent;
|
|
import org.tasks.notifications.NotificationManager;
|
|
import org.tasks.preferences.Preferences;
|
|
import org.tasks.sync.RecordSyncStatusCallback;
|
|
import org.tasks.time.DateTime;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import javax.inject.Inject;
|
|
|
|
import timber.log.Timber;
|
|
|
|
import static org.tasks.date.DateTimeUtils.newDateTime;
|
|
|
|
/**
|
|
* Define a sync adapter for the app.
|
|
*
|
|
* <p>This class is instantiated in {@link GoogleTaskSyncService}, which also binds SyncAdapter to the system.
|
|
* SyncAdapter should only be initialized in SyncService, never anywhere else.
|
|
*
|
|
* <p>The system calls onPerformSync() via an RPC call through the IBinder object supplied by
|
|
* SyncService.
|
|
*/
|
|
public class GoogleTaskSyncAdapter extends InjectingAbstractThreadedSyncAdapter {
|
|
|
|
private static final String DEFAULT_LIST = "@default"; //$NON-NLS-1$
|
|
|
|
@Inject GtasksPreferenceService gtasksPreferenceService;
|
|
@Inject LocalBroadcastManager localBroadcastManager;
|
|
@Inject StoreObjectDao storeObjectDao;
|
|
@Inject GtasksSyncService gtasksSyncService;
|
|
@Inject GtasksListService gtasksListService;
|
|
@Inject GtasksTaskListUpdater gtasksTaskListUpdater;
|
|
@Inject Preferences preferences;
|
|
@Inject GtasksInvoker gtasksInvoker;
|
|
@Inject TaskDao taskDao;
|
|
@Inject MetadataDao metadataDao;
|
|
@Inject GtasksMetadata gtasksMetadataFactory;
|
|
@Inject Tracker tracker;
|
|
@Inject NotificationManager notificationManager;
|
|
|
|
public GoogleTaskSyncAdapter(Context context, boolean autoInitialize) {
|
|
super(context, autoInitialize);
|
|
}
|
|
|
|
/**
|
|
* Called by the Android system in response to a request to run the sync adapter. The work
|
|
* required to read data from the network, parse it, and store it in the content provider is
|
|
* done here. Extending AbstractThreadedSyncAdapter ensures that all methods within SyncAdapter
|
|
* run on a background thread. For this reason, blocking I/O and other long-running tasks can be
|
|
* run <em>in situ</em>, and you don't have to set up a separate thread for them.
|
|
.
|
|
*
|
|
* <p>This is where we actually perform any work required to perform a sync.
|
|
* {@link android.content.AbstractThreadedSyncAdapter} guarantees that this will be called on a non-UI thread,
|
|
* so it is safe to peform blocking I/O here.
|
|
*
|
|
* <p>The syncResult argument allows you to pass information back to the method that triggered
|
|
* the sync.
|
|
*/
|
|
@Override
|
|
public void onPerformSync(Account account, Bundle extras, String authority,
|
|
ContentProviderClient provider, SyncResult syncResult) {
|
|
if (!account.name.equals(gtasksPreferenceService.getUserName())) {
|
|
Timber.d("Sync not enabled for %s", account);
|
|
syncResult.stats.numAuthExceptions++;
|
|
return;
|
|
}
|
|
Timber.d("%s: start sync", account);
|
|
RecordSyncStatusCallback callback = new RecordSyncStatusCallback(gtasksPreferenceService, localBroadcastManager);
|
|
try {
|
|
callback.started();
|
|
synchronize();
|
|
gtasksPreferenceService.recordSuccessfulSync();
|
|
} catch (UserRecoverableAuthIOException e) {
|
|
Timber.e(e, e.getMessage());
|
|
sendNotification(getContext(), e.getIntent());
|
|
} catch (IOException e) {
|
|
Timber.e(e, e.getMessage());
|
|
} catch (Exception e) {
|
|
tracker.reportException(e);
|
|
} finally {
|
|
callback.finished();
|
|
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).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(android.R.drawable.ic_dialog_alert)
|
|
.setTicker(context.getString(R.string.common_google_play_services_notification_ticker));
|
|
notificationManager.notify(Constants.NOTIFICATION_SYNC_ERROR, builder.build());
|
|
}
|
|
|
|
private void synchronize() throws IOException {
|
|
pushLocalChanges();
|
|
|
|
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(gtaskLists);
|
|
if (gtasksListService.getList(gtasksPreferenceService.getDefaultList()) == null) {
|
|
gtasksPreferenceService.setDefaultList(null);
|
|
}
|
|
for (final GtasksList list : gtasksListService.getListsToUpdate(gtaskLists)) {
|
|
fetchAndApplyRemoteChanges(list);
|
|
}
|
|
}
|
|
|
|
private void pushLocalChanges() throws UserRecoverableAuthIOException {
|
|
List<Task> tasks = taskDao.toList(Query.select(Task.PROPERTIES)
|
|
.join(Join.left(Metadata.TABLE, Criterion.and(MetadataDao.MetadataCriteria.withKey(GtasksMetadata.METADATA_KEY), Task.ID.eq(Metadata.TASK))))
|
|
.where(Criterion.or(Task.MODIFICATION_DATE.gt(GtasksMetadata.LAST_SYNC), GtasksMetadata.ID.eq(""))));
|
|
for (Task task : tasks) {
|
|
try {
|
|
pushTask(task, task.getMergedValues(), gtasksInvoker);
|
|
} catch (UserRecoverableAuthIOException e) {
|
|
throw e;
|
|
} catch (IOException e) {
|
|
Timber.e(e, e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synchronize with server when data changes
|
|
*/
|
|
private void pushTask(Task task, ContentValues values, GtasksInvoker invoker) throws IOException {
|
|
for (Metadata deleted : getDeleted(task.getId())) {
|
|
gtasksInvoker.deleteGtask(deleted.getValue(GtasksMetadata.LIST_ID), deleted.getValue(GtasksMetadata.ID));
|
|
metadataDao.delete(deleted.getId());
|
|
}
|
|
|
|
Metadata gtasksMetadata = metadataDao.getFirstActiveByTaskAndKey(task.getId(), GtasksMetadata.METADATA_KEY);
|
|
com.google.api.services.tasks.model.Task remoteModel;
|
|
boolean newlyCreated = false;
|
|
|
|
String remoteId;
|
|
String listId = gtasksPreferenceService.getDefaultList();
|
|
if (listId == null) {
|
|
com.google.api.services.tasks.model.TaskList defaultList = invoker.getGtaskList(DEFAULT_LIST);
|
|
if (defaultList != null) {
|
|
listId = defaultList.getId();
|
|
gtasksPreferenceService.setDefaultList(listId);
|
|
} else {
|
|
listId = DEFAULT_LIST;
|
|
}
|
|
}
|
|
|
|
if (gtasksMetadata == null || !gtasksMetadata.containsNonNullValue(GtasksMetadata.ID) ||
|
|
TextUtils.isEmpty(gtasksMetadata.getValue(GtasksMetadata.ID))) { //Create case
|
|
if (gtasksMetadata == null) {
|
|
gtasksMetadata = gtasksMetadataFactory.createEmptyMetadata(task.getId());
|
|
}
|
|
if (gtasksMetadata.containsNonNullValue(GtasksMetadata.LIST_ID)) {
|
|
listId = gtasksMetadata.getValue(GtasksMetadata.LIST_ID);
|
|
}
|
|
|
|
remoteModel = new com.google.api.services.tasks.model.Task();
|
|
newlyCreated = true;
|
|
} else { //update case
|
|
remoteId = gtasksMetadata.getValue(GtasksMetadata.ID);
|
|
listId = gtasksMetadata.getValue(GtasksMetadata.LIST_ID);
|
|
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 &&
|
|
(!values.containsKey(Task.TITLE.name) || TextUtils.isEmpty(task.getTitle()) || task.getDeletionDate() > 0)) {
|
|
return;
|
|
}
|
|
|
|
//Update the remote model's changed properties
|
|
if (values.containsKey(Task.DELETION_DATE.name) && task.isDeleted()) {
|
|
remoteModel.setDeleted(true);
|
|
}
|
|
|
|
if (values.containsKey(Task.TITLE.name)) {
|
|
remoteModel.setTitle(task.getTitle());
|
|
}
|
|
if (values.containsKey(Task.NOTES.name)) {
|
|
remoteModel.setNotes(task.getNotes());
|
|
}
|
|
if (values.containsKey(Task.DUE_DATE.name) && task.hasDueDate()) {
|
|
remoteModel.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(task.getDueDate()));
|
|
}
|
|
if (values.containsKey(Task.COMPLETION_DATE.name)) {
|
|
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 {
|
|
invoker.updateGtask(listId, remoteModel);
|
|
} catch(HttpNotFoundException e) {
|
|
Timber.e(e, e.getMessage());
|
|
metadataDao.delete(gtasksMetadata.getId());
|
|
return;
|
|
}
|
|
} else {
|
|
String parent = gtasksSyncService.getRemoteParentId(gtasksMetadata);
|
|
String priorSibling = gtasksSyncService.getRemoteSiblingId(listId, gtasksMetadata);
|
|
|
|
com.google.api.services.tasks.model.Task created = invoker.createGtask(listId, remoteModel, parent, priorSibling);
|
|
|
|
if (created != null) {
|
|
//Update the metadata for the newly created task
|
|
gtasksMetadata.setValue(GtasksMetadata.ID, created.getId());
|
|
gtasksMetadata.setValue(GtasksMetadata.LIST_ID, listId);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
task.setModificationDate(DateUtilities.now());
|
|
gtasksMetadata.setValue(GtasksMetadata.LAST_SYNC, DateUtilities.now() + 1000L);
|
|
metadataDao.persist(gtasksMetadata);
|
|
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
|
|
taskDao.saveExistingWithSqlConstraintCheck(task);
|
|
}
|
|
|
|
private List<Metadata> getDeleted(long taskId) {
|
|
return metadataDao.toList(Criterion.and(
|
|
MetadataDao.MetadataCriteria.byTaskAndwithKey(taskId, GtasksMetadata.METADATA_KEY),
|
|
MetadataDao.MetadataCriteria.isDeleted()));
|
|
}
|
|
|
|
private synchronized void fetchAndApplyRemoteChanges(GtasksList 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 t : tasks) {
|
|
GtasksTaskContainer container = new GtasksTaskContainer(t, listId, GtasksMetadata.createEmptyMetadataWithoutList(AbstractModel.NO_ID));
|
|
findLocalMatch(container);
|
|
container.gtaskMetadata.setValue(GtasksMetadata.GTASKS_ORDER, Long.parseLong(t.getPosition()));
|
|
container.gtaskMetadata.setValue(GtasksMetadata.PARENT_TASK, localIdForGtasksId(t.getParent()));
|
|
container.gtaskMetadata.setValue(GtasksMetadata.LAST_SYNC, DateUtilities.now() + 1000L);
|
|
write(container);
|
|
lastSyncDate = Math.max(lastSyncDate, container.getUpdateTime());
|
|
}
|
|
list.setLastSync(lastSyncDate);
|
|
storeObjectDao.persist(list);
|
|
gtasksTaskListUpdater.correctOrderAndIndentForList(listId);
|
|
}
|
|
} catch (UserRecoverableAuthIOException e) {
|
|
throw e;
|
|
} catch (IOException e) {
|
|
Timber.e(e, e.getMessage());
|
|
}
|
|
}
|
|
|
|
private long localIdForGtasksId(String gtasksId) {
|
|
Metadata metadata = getMetadataByGtaskId(gtasksId);
|
|
return metadata == null ? AbstractModel.NO_ID : metadata.getTask();
|
|
}
|
|
|
|
private void findLocalMatch(GtasksTaskContainer remoteTask) {
|
|
if(remoteTask.task.getId() != Task.NO_ID) {
|
|
return;
|
|
}
|
|
Metadata metadata = getMetadataByGtaskId(remoteTask.gtaskMetadata.getValue(GtasksMetadata.ID));
|
|
if (metadata != null) {
|
|
remoteTask.task.setId(metadata.getValue(Metadata.TASK));
|
|
remoteTask.task.setUuid(taskDao.uuidFromLocalId(remoteTask.task.getId()));
|
|
remoteTask.gtaskMetadata = metadata;
|
|
}
|
|
}
|
|
|
|
private Metadata getMetadataByGtaskId(String gtaskId) {
|
|
return metadataDao.getFirst(Query.select(Metadata.PROPERTIES).where(Criterion.and(
|
|
Metadata.KEY.eq(GtasksMetadata.METADATA_KEY),
|
|
GtasksMetadata.ID.eq(gtaskId))));
|
|
}
|
|
|
|
private void write(GtasksTaskContainer task) {
|
|
// merge astrid dates with google dates
|
|
|
|
if(task.task.isSaved()) {
|
|
Task local = taskDao.fetch(task.task.getId(), Task.PROPERTIES);
|
|
if (local == null) {
|
|
task.task.clearValue(Task.ID);
|
|
task.task.clearValue(Task.UUID);
|
|
} else {
|
|
mergeDates(task.task, local);
|
|
}
|
|
} else { // Set default importance and reminders for remotely created tasks
|
|
task.task.setImportance(preferences.getIntegerFromString(
|
|
R.string.p_default_importance_key, Task.IMPORTANCE_SHOULD_DO));
|
|
TaskDao.setDefaultReminders(preferences, task.task);
|
|
}
|
|
if (!TextUtils.isEmpty(task.task.getTitle())) {
|
|
task.task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
|
|
task.task.putTransitory(TaskDao.TRANS_SUPPRESS_REFRESH, true);
|
|
saveTaskAndMetadata(task);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves a task and its metadata
|
|
*/
|
|
private void saveTaskAndMetadata(GtasksTaskContainer task) {
|
|
task.prepareForSaving();
|
|
taskDao.save(task.task);
|
|
synchronizeMetadata(task.task.getId(), task.metadata, GtasksMetadata.METADATA_KEY);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @param metadataKey metadata key
|
|
*/
|
|
private void synchronizeMetadata(long taskId, ArrayList<Metadata> metadata, String metadataKey) {
|
|
final Set<ContentValues> newMetadataValues = new HashSet<>();
|
|
for(Metadata metadatum : metadata) {
|
|
metadatum.setTask(taskId);
|
|
metadatum.clearValue(Metadata.ID);
|
|
newMetadataValues.add(metadatum.getMergedValues());
|
|
}
|
|
|
|
metadataDao.byTaskAndKey(taskId, metadataKey, item -> {
|
|
long id = item.getId();
|
|
|
|
// clear item id when matching with incoming values
|
|
item.clearValue(Metadata.ID);
|
|
ContentValues itemMergedValues = item.getMergedValues();
|
|
if(newMetadataValues.contains(itemMergedValues)) {
|
|
newMetadataValues.remove(itemMergedValues);
|
|
} else {
|
|
// not matched. cut it
|
|
metadataDao.delete(id);
|
|
}
|
|
});
|
|
|
|
// everything that remains shall be written
|
|
for(ContentValues values : newMetadataValues) {
|
|
Metadata item = new Metadata();
|
|
item.mergeWith(values);
|
|
metadataDao.persist(item);
|
|
}
|
|
}
|
|
|
|
|
|
static void mergeDates(Task remote, Task local) {
|
|
if (remote.hasDueDate() && local.hasDueTime()) {
|
|
DateTime oldDate = newDateTime(local.getDueDate());
|
|
DateTime newDate = newDateTime(remote.getDueDate())
|
|
.withHourOfDay(oldDate.getHourOfDay())
|
|
.withMinuteOfHour(oldDate.getMinuteOfHour())
|
|
.withSecondOfMinute(oldDate.getSecondOfMinute());
|
|
local.setDueDateAdjustingHideUntil(
|
|
Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDate.getMillis()));
|
|
} else {
|
|
local.setDueDateAdjustingHideUntil(remote.getDueDate());
|
|
}
|
|
|
|
remote.setHideUntil(local.getHideUntil());
|
|
remote.setDueDate(local.getDueDate());
|
|
}
|
|
|
|
@Override
|
|
protected void inject(SyncAdapterComponent component) {
|
|
component.inject(this);
|
|
}
|
|
}
|