mirror of https://github.com/tasks/tasks
Remove unused code
parent
e8b1eff7d7
commit
e7d629e365
@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.andlib.data;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
import com.todoroo.andlib.sql.Criterion;
|
||||
import com.todoroo.andlib.sql.Query;
|
||||
import com.todoroo.andlib.utility.AndroidUtilities;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* DAO for reading and writing values from an Android ContentResolver
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
* @param <TYPE> model type
|
||||
*/
|
||||
public class ContentResolverDao<TYPE extends AbstractModel> {
|
||||
|
||||
/** class of model */
|
||||
private final Class<TYPE> modelClass;
|
||||
|
||||
/** base content uri */
|
||||
private final Uri baseUri;
|
||||
|
||||
/** content resolver */
|
||||
private final ContentResolver cr;
|
||||
|
||||
@Autowired
|
||||
protected Boolean debug;
|
||||
|
||||
public ContentResolverDao(Class<TYPE> modelClass, Context context, Uri baseUri) {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
this.modelClass = modelClass;
|
||||
if(debug == null) {
|
||||
debug = false;
|
||||
}
|
||||
this.baseUri = baseUri;
|
||||
|
||||
cr = context.getContentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI for a single id
|
||||
*/
|
||||
private Uri uriWithId(long id) {
|
||||
return Uri.withAppendedPath(baseUri, Long.toString(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete specific item from the given table
|
||||
* @return number of rows affected
|
||||
*/
|
||||
public int delete(long id) {
|
||||
return cr.delete(uriWithId(id), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete by criteria
|
||||
* @return number of rows affected
|
||||
*/
|
||||
public int deleteWhere(Criterion where) {
|
||||
return cr.delete(baseUri, where.toString(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query content provider
|
||||
*/
|
||||
public TodorooCursor<TYPE> query(Query query) {
|
||||
if(debug) {
|
||||
Log.i("SQL-" + modelClass.getSimpleName(), query.toString()); //$NON-NLS-1$
|
||||
}
|
||||
Cursor cursor = query.queryContentResolver(cr, baseUri);
|
||||
return new TodorooCursor<TYPE>(cursor, query.getFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new or save existing model
|
||||
* @return true if data was written to the db, false otherwise
|
||||
*/
|
||||
public boolean save(TYPE model) {
|
||||
writeTransitoriesToModelContentValues(model);
|
||||
if(model.isSaved()) {
|
||||
if(model.getSetValues() == null) {
|
||||
return false;
|
||||
}
|
||||
if(cr.update(uriWithId(model.getId()), model.getSetValues(), null, null) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Uri uri = cr.insert(baseUri, model.getMergedValues());
|
||||
long id = Long.parseLong(uri.getLastPathSegment());
|
||||
model.setId(id);
|
||||
model.markSaved();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void writeTransitoriesToModelContentValues(AbstractModel model) {
|
||||
Set<String> keys = model.getAllTransitoryKeys();
|
||||
if (keys != null) {
|
||||
ContentValues transitories = new ContentValues();
|
||||
for (String key : keys) {
|
||||
String newKey = AbstractModel.RETAIN_TRANSITORY_PREFIX + key;
|
||||
Object value = model.getTransitory(key);
|
||||
AndroidUtilities.putInto(transitories, newKey, value, false);
|
||||
}
|
||||
model.mergeWith(transitories);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object corresponding to the given identifier
|
||||
*
|
||||
* @param properties
|
||||
* properties to read
|
||||
* @param id
|
||||
* id of item
|
||||
* @return null if no item found
|
||||
*/
|
||||
public TYPE fetch(long id, Property<?>... properties) {
|
||||
TodorooCursor<TYPE> cursor = query(
|
||||
Query.select(properties).where(AbstractModel.ID_PROPERTY.eq(id)));
|
||||
try {
|
||||
if (cursor.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
cursor.moveToFirst();
|
||||
Constructor<TYPE> constructor = modelClass.getConstructor(TodorooCursor.class);
|
||||
return constructor.newInstance(cursor);
|
||||
} catch (SecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
try {
|
||||
cursor.close();
|
||||
} catch (NullPointerException e) {
|
||||
// cursor was not open
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.andlib.sql;
|
||||
|
||||
public class EqCriterion extends UnaryCriterion {
|
||||
EqCriterion(Field field, Object value) {
|
||||
super(field, Operator.eq, value);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.andlib.sql;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GroupBy {
|
||||
private List<Field> fields = new ArrayList<Field>();
|
||||
|
||||
public static GroupBy groupBy(Field field) {
|
||||
GroupBy groupBy = new GroupBy();
|
||||
groupBy.fields.add(field);
|
||||
return groupBy;
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.api;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Section Header for Filter List
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class FilterListHeader extends FilterListItem {
|
||||
|
||||
/**
|
||||
* Constructor for creating a new FilterListHeader
|
||||
*/
|
||||
public FilterListHeader(String listingTitle) {
|
||||
this.listingTitle = listingTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a new FilterListHeader
|
||||
*/
|
||||
protected FilterListHeader() {
|
||||
//
|
||||
}
|
||||
|
||||
// --- parcelable
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<FilterListHeader> CREATOR = new Parcelable.Creator<FilterListHeader>() {
|
||||
|
||||
@Override
|
||||
public FilterListHeader createFromParcel(Parcel source) {
|
||||
FilterListHeader item = new FilterListHeader();
|
||||
item.readFromParcel(source);
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterListHeader[] newArray(int size) {
|
||||
return new FilterListHeader[size];
|
||||
}
|
||||
|
||||
};
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.api;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
|
||||
/**
|
||||
* Special filter that launches a PendingIntent when accessed.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public final class IntentFilter extends FilterListItem implements Parcelable {
|
||||
|
||||
/**
|
||||
* PendingIntent to trigger when pressed
|
||||
*/
|
||||
public PendingIntent intent;
|
||||
|
||||
/**
|
||||
* Constructor for creating a new IntentFilter
|
||||
*
|
||||
* @param listingTitle
|
||||
* Title of this item as displayed on the lists page, e.g. Inbox
|
||||
* @param intent
|
||||
* intent to load
|
||||
*/
|
||||
public IntentFilter(String listingTitle, PendingIntent intent) {
|
||||
this.listingTitle = listingTitle;
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a new IntentFilter used internally
|
||||
*/
|
||||
protected IntentFilter(PendingIntent intent) {
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
// --- parcelable
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(intent, 0);
|
||||
super.writeToParcel(dest, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable creator
|
||||
*/
|
||||
public static final Parcelable.Creator<IntentFilter> CREATOR = new Parcelable.Creator<IntentFilter>() {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public IntentFilter createFromParcel(Parcel source) {
|
||||
IntentFilter item = new IntentFilter((PendingIntent) source.readParcelable(
|
||||
PendingIntent.class.getClassLoader()));
|
||||
item.readFromParcel(source);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public IntentFilter[] newArray(int size) {
|
||||
return new IntentFilter[size];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.core;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.todoroo.astrid.api.FilterListItem;
|
||||
|
||||
/**
|
||||
* Special filter that triggers the search functionality when accessed.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class SearchFilter extends FilterListItem {
|
||||
|
||||
/**
|
||||
* Constructor for creating a new SearchFilter
|
||||
*
|
||||
* @param listingTitle
|
||||
* Title of this item as displayed on the lists page, e.g. Inbox
|
||||
*/
|
||||
public SearchFilter(String listingTitle) {
|
||||
this.listingTitle = listingTitle;
|
||||
}
|
||||
|
||||
protected SearchFilter() {
|
||||
//
|
||||
}
|
||||
|
||||
// --- parcelable
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable creator
|
||||
*/
|
||||
public static final Parcelable.Creator<SearchFilter> CREATOR = new Parcelable.Creator<SearchFilter>() {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public SearchFilter createFromParcel(Parcel source) {
|
||||
SearchFilter item = new SearchFilter();
|
||||
item.readFromParcel(source);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public SearchFilter[] newArray(int size) {
|
||||
return new SearchFilter[size];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.todoroo.andlib.data.ContentResolverDao;
|
||||
import com.todoroo.andlib.sql.Criterion;
|
||||
|
||||
/**
|
||||
* Data access object for accessing Astrid's {@link Metadata} table. A
|
||||
* piece of Metadata is information about a task, for example a tag or a
|
||||
* note. It operates in a one-to-many relation with tasks.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class MetadataApiDao extends ContentResolverDao<Metadata> {
|
||||
|
||||
public MetadataApiDao(Context context) {
|
||||
super(Metadata.class, context, Metadata.CONTENT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL clauses
|
||||
*/
|
||||
public static class MetadataCriteria {
|
||||
|
||||
/** Returns all metadata associated with a given task */
|
||||
public static Criterion byTask(long taskId) {
|
||||
return Metadata.TASK.eq(taskId);
|
||||
}
|
||||
|
||||
/** Returns all metadata associated with a given key */
|
||||
public static Criterion withKey(String key) {
|
||||
return Metadata.KEY.eq(key);
|
||||
}
|
||||
|
||||
/** Returns all metadata associated with a given key */
|
||||
public static Criterion byTaskAndwithKey(long taskId, String key) {
|
||||
return Criterion.and(withKey(key), byTask(taskId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.todoroo.andlib.data.ContentResolverDao;
|
||||
import com.todoroo.andlib.sql.Criterion;
|
||||
|
||||
/**
|
||||
* Data access object for accessing Astrid's {@link StoreObject} table. A
|
||||
* StoreObject is an arbitrary piece of data stored inside of Astrid.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class StoreObjectApiDao extends ContentResolverDao<StoreObject> {
|
||||
|
||||
public StoreObjectApiDao(Context context) {
|
||||
super(StoreObject.class, context, StoreObject.CONTENT_URI);
|
||||
}
|
||||
|
||||
// --- SQL clause generators
|
||||
|
||||
/**
|
||||
* Generates SQL clauses
|
||||
*/
|
||||
public static class StoreObjectCriteria {
|
||||
|
||||
/** Returns all store objects with given type */
|
||||
public static Criterion byType(String type) {
|
||||
return StoreObject.TYPE.eq(type);
|
||||
}
|
||||
|
||||
/** Returns store object with type and key */
|
||||
public static Criterion byTypeAndItem(String type, String item) {
|
||||
return Criterion.and(byType(type), StoreObject.ITEM.eq(item));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.sync;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.ContextManager;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
import com.todoroo.andlib.service.ExceptionService;
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
import com.todoroo.andlib.utility.Preferences;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Performs synchronization service logic in background service to avoid
|
||||
* ANR (application not responding) messages.
|
||||
* <p>
|
||||
* Starting this service
|
||||
* schedules a repeating alarm which handles
|
||||
* synchronization with your serv
|
||||
*
|
||||
* @author Tim Su
|
||||
*
|
||||
*/
|
||||
abstract public class SyncBackgroundService extends Service {
|
||||
|
||||
/** Minimum time before an auto-sync */
|
||||
private static final long AUTO_SYNC_MIN_OFFSET = 5*60*1000L;
|
||||
|
||||
@Autowired private ExceptionService exceptionService;
|
||||
|
||||
// --- abstract methods
|
||||
|
||||
abstract protected SyncProvider<?> getSyncProvider();
|
||||
|
||||
abstract protected SyncProviderUtilities getSyncUtilities();
|
||||
|
||||
// --- implementation
|
||||
|
||||
public SyncBackgroundService() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
}
|
||||
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
/** Receive the alarm - start the synchronize service! */
|
||||
@Override
|
||||
public void onStart(Intent intent, int startId) {
|
||||
try {
|
||||
if(intent != null && !started.getAndSet(true)) {
|
||||
startSynchronization(this);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exceptionService.reportError(getSyncUtilities().getIdentifier() + "-bg-sync", e); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
/** Start the actual synchronization */
|
||||
private void startSynchronization(Context context) {
|
||||
if(context == null || context.getResources() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContextManager.setContext(context);
|
||||
|
||||
if(!getSyncUtilities().isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getSyncProvider().synchronize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
started.set(false);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
// --- alarm management
|
||||
|
||||
/**
|
||||
* Schedules repeating alarm for auto-synchronization
|
||||
*/
|
||||
public void scheduleService() {
|
||||
int syncFrequencySeconds = 0;
|
||||
try {
|
||||
syncFrequencySeconds = Preferences.getIntegerFromString(
|
||||
getSyncUtilities().getSyncIntervalKey(), -1);
|
||||
} catch(ClassCastException e) {
|
||||
Preferences.setStringFromInteger(getSyncUtilities().getSyncIntervalKey(), 0);
|
||||
}
|
||||
Context context = ContextManager.getContext();
|
||||
if(syncFrequencySeconds <= 0) {
|
||||
unscheduleService(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// figure out synchronization frequency
|
||||
long interval = 1000L * syncFrequencySeconds;
|
||||
long offset = computeNextSyncOffset(interval);
|
||||
|
||||
// give a little padding
|
||||
offset = Math.max(offset, AUTO_SYNC_MIN_OFFSET);
|
||||
|
||||
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
PendingIntent pendingIntent = PendingIntent.getService(context, getSyncUtilities().getSyncIntervalKey(),
|
||||
createAlarmIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Log.i("Astrid", "Autosync set for " + offset / 1000 //$NON-NLS-1$ //$NON-NLS-2$
|
||||
+ " seconds repeating every " + syncFrequencySeconds); //$NON-NLS-1$
|
||||
|
||||
// cancel all existing
|
||||
am.cancel(pendingIntent);
|
||||
|
||||
// schedule new
|
||||
am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + offset,
|
||||
interval, pendingIntent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes repeating alarm for auto-synchronization
|
||||
*/
|
||||
private void unscheduleService(Context context) {
|
||||
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
|
||||
PendingIntent pendingIntent = PendingIntent.getService(context, getSyncUtilities().getSyncIntervalKey(),
|
||||
createAlarmIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
am.cancel(pendingIntent);
|
||||
}
|
||||
|
||||
/** Create the alarm intent */
|
||||
private Intent createAlarmIntent(Context context) {
|
||||
Intent intent = new Intent(context, getClass());
|
||||
return intent;
|
||||
}
|
||||
|
||||
// --- utility methods
|
||||
|
||||
private long computeNextSyncOffset(long interval) {
|
||||
// figure out last synchronize time
|
||||
long lastSyncDate = getSyncUtilities().getLastSyncDate();
|
||||
|
||||
// if user never synchronized, give them a full offset period before bg sync
|
||||
if(lastSyncDate != 0) {
|
||||
return Math.max(0, lastSyncDate + interval - DateUtilities.now());
|
||||
} else {
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2012 Todoroo Inc
|
||||
*
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.sync;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.todoroo.andlib.data.Property.LongProperty;
|
||||
import com.todoroo.andlib.data.TodorooCursor;
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.ContextManager;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
import com.todoroo.andlib.service.ExceptionService;
|
||||
import com.todoroo.andlib.service.NotificationManager;
|
||||
import com.todoroo.andlib.utility.DialogUtilities;
|
||||
import com.todoroo.astrid.data.Task;
|
||||
|
||||
import org.tasks.api.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A helper class for writing synchronization services for Astrid. This class
|
||||
* contains logic for merging incoming changes and writing outgoing changes.
|
||||
* <p>
|
||||
* Use {@link #initiateManual} as the entry point for your synchronization
|
||||
* service, which should check if a user is logged in. If not, you should
|
||||
* handle that in the UI, otherwise, you should launch your background
|
||||
* service to perform synchronization in the background.
|
||||
* <p>
|
||||
* Your background service should {@link #synchronize}, which in turn
|
||||
* invokes {@link #initiateBackground} to initiate synchronization.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public abstract class SyncProvider<TYPE extends SyncContainer> {
|
||||
|
||||
// --- abstract methods - your services should implement these
|
||||
|
||||
/**
|
||||
* @return sync utility instance
|
||||
*/
|
||||
abstract protected SyncProviderUtilities getUtilities();
|
||||
|
||||
/**
|
||||
* Perform log in (launching activity if necessary) and sync. This is
|
||||
* invoked when users manually request synchronization
|
||||
*
|
||||
* @param activity
|
||||
* context
|
||||
*/
|
||||
abstract protected void initiateManual(Activity activity);
|
||||
|
||||
/**
|
||||
* Perform synchronize. Since this can be called from background services,
|
||||
* you should not open up new activities. Instead, if the user is not signed
|
||||
* in, your service should do nothing.
|
||||
*/
|
||||
abstract protected void initiateBackground();
|
||||
|
||||
/**
|
||||
* Updates the text of a notification and the intent to open when tapped
|
||||
* @return notification id (in Android, there is at most one notification
|
||||
* in the tray for a given id)
|
||||
*/
|
||||
abstract protected int updateNotification(Context context, Notification n);
|
||||
|
||||
/**
|
||||
* Create a task on the remote server.
|
||||
*
|
||||
* @param task
|
||||
* task to create
|
||||
*/
|
||||
abstract protected void create(TYPE task);
|
||||
|
||||
/**
|
||||
* Push variables from given task to the remote server, and read the newly
|
||||
* updated task.
|
||||
*
|
||||
* @param task
|
||||
* task proxy to push
|
||||
* @param remote
|
||||
* remote task that we merged with. may be null
|
||||
* @return task pulled on remote server
|
||||
*/
|
||||
abstract protected TYPE push(TYPE task, TYPE remote);
|
||||
|
||||
/**
|
||||
* Fetch remote task. Used to re-read merged tasks
|
||||
*
|
||||
* @param task
|
||||
* task with id's to re-read
|
||||
* @return new Task
|
||||
*/
|
||||
abstract protected TYPE pull(TYPE task);
|
||||
|
||||
/**
|
||||
* Reads a task container from a task in the database
|
||||
*/
|
||||
abstract protected TYPE read(TodorooCursor<Task> task);
|
||||
|
||||
/**
|
||||
* Save task. Used to save local tasks that have been updated and remote
|
||||
* tasks that need to be created locally
|
||||
*/
|
||||
abstract protected void write(TYPE task);
|
||||
|
||||
/**
|
||||
* Finds a task in the list with the same remote identifier(s) as
|
||||
* the task passed in
|
||||
*
|
||||
* @return task from list if matches, null otherwise
|
||||
*/
|
||||
abstract protected int matchTask(ArrayList<TYPE> tasks, TYPE target);
|
||||
|
||||
/**
|
||||
* Transfer remote identifier(s) from one task to another
|
||||
*/
|
||||
abstract protected void transferIdentifiers(TYPE source,
|
||||
TYPE destination);
|
||||
|
||||
// --- implementation
|
||||
|
||||
private final Notification notification;
|
||||
|
||||
@Autowired protected ExceptionService exceptionService;
|
||||
|
||||
public SyncProvider() {
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
|
||||
// initialize notification
|
||||
int icon = android.R.drawable.stat_notify_sync;
|
||||
long when = System.currentTimeMillis();
|
||||
notification = new Notification(icon, null, when);
|
||||
notification.flags |= Notification.FLAG_ONGOING_EVENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize this provider with sync toast
|
||||
*/
|
||||
public void synchronize(final Context context) {
|
||||
synchronize(context, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize this provider
|
||||
* @param showSyncToast should we toast to indicate synchronizing?
|
||||
*/
|
||||
public void synchronize(final Context context, final boolean showSyncToast) {
|
||||
// display toast
|
||||
if(context instanceof Activity) {
|
||||
if(getUtilities().isLoggedIn()) {
|
||||
((Activity) context).runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if(showSyncToast) {
|
||||
makeSyncToast(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
initiateManual((Activity)context);
|
||||
} else if(context instanceof SyncBackgroundService) {
|
||||
// display notification
|
||||
final int notificationId = updateNotification(context, notification);
|
||||
final NotificationManager nm = new NotificationManager.AndroidNotificationManager(context);
|
||||
nm.notify(notificationId, notification);
|
||||
|
||||
// start next step in background thread
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
initiateBackground();
|
||||
} finally {
|
||||
nm.cancel(notificationId);
|
||||
((SyncBackgroundService)context).stop();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
// unit test
|
||||
initiateBackground();
|
||||
}
|
||||
}
|
||||
|
||||
protected void makeSyncToast(Context context) {
|
||||
Toast.makeText(context, R.string.SyP_progress_toast,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
// --- synchronization logic
|
||||
|
||||
/**
|
||||
* Helper to synchronize remote tasks with our local database.
|
||||
*
|
||||
* This initiates the following process: 1. local changes are read 2. remote
|
||||
* changes are read 3. local tasks are merged with remote changes and pushed
|
||||
* across 4. remote changes are then read in
|
||||
*
|
||||
* @param data synchronization data structure
|
||||
*/
|
||||
protected void synchronizeTasks(SyncData<TYPE> data) {
|
||||
int length;
|
||||
|
||||
// create internal data structures
|
||||
HashMap<String, Integer> remoteNewTaskNameMap = new HashMap<String, Integer>();
|
||||
length = data.remoteUpdated.size();
|
||||
for(int i = 0; i < length; i++) {
|
||||
TYPE remote = data.remoteUpdated.get(i);
|
||||
if(remote.task.getId() != Task.NO_ID) {
|
||||
continue;
|
||||
}
|
||||
remoteNewTaskNameMap.put(remote.task.getValue(Task.TITLE), i);
|
||||
}
|
||||
|
||||
// 1. CREATE: grab newly created tasks and create them remotely
|
||||
sendLocallyCreated(data, remoteNewTaskNameMap);
|
||||
|
||||
// 2. UPDATE: for each updated local task
|
||||
sendLocallyUpdated(data);
|
||||
|
||||
// 3. REMOTE: load remote information
|
||||
readRemotelyUpdated(data);
|
||||
}
|
||||
|
||||
protected String getFinalSyncStatus() {
|
||||
if (getUtilities().getLastError() != null || getUtilities().getLastAttemptedSyncDate() != 0) {
|
||||
if (getUtilities().getLastAttemptedSyncDate() == 0) {
|
||||
return "errors";
|
||||
} else {
|
||||
return "failed";
|
||||
}
|
||||
} else {
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
|
||||
protected void readRemotelyUpdated(SyncData<TYPE> data) {
|
||||
int length;
|
||||
// Rearrange remoteTasks so completed tasks get synchronized first.
|
||||
// This prevents bugs where a repeated task has two copies come down
|
||||
// the wire, the new version and the completed old version. The new
|
||||
// version would get merged, then completed, if done in the wrong order.
|
||||
|
||||
Collections.sort(data.remoteUpdated, new Comparator<TYPE>() {
|
||||
private static final int SENTINEL = -2;
|
||||
private final int check(TYPE o1, TYPE o2, LongProperty property) {
|
||||
long o1Property = o1.task.getValue(property);
|
||||
long o2Property = o2.task.getValue(property);
|
||||
if(o1Property != 0 && o2Property != 0) {
|
||||
return 0;
|
||||
} else if(o1Property != 0) {
|
||||
return -1;
|
||||
} else if(o2Property != 0) {
|
||||
return 1;
|
||||
}
|
||||
return SENTINEL;
|
||||
}
|
||||
@Override
|
||||
public int compare(TYPE o1, TYPE o2) {
|
||||
int comparison = check(o1, o2, Task.DELETION_DATE);
|
||||
if(comparison != SENTINEL) {
|
||||
return comparison;
|
||||
}
|
||||
comparison = check(o1, o2, Task.COMPLETION_DATE);
|
||||
if(comparison != SENTINEL) {
|
||||
return comparison;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
length = data.remoteUpdated.size();
|
||||
for(int i = 0; i < length; i++) {
|
||||
TYPE remote = data.remoteUpdated.get(i);
|
||||
|
||||
// don't synchronize new & deleted tasks
|
||||
if(!remote.task.isSaved() && (remote.task.isDeleted())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
write(remote);
|
||||
} catch (Exception e) {
|
||||
handleException("sync-remote-updated", e, false); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendLocallyUpdated(SyncData<TYPE> data) {
|
||||
int length;
|
||||
length = data.localUpdated.getCount();
|
||||
for(int i = 0; i < length; i++) {
|
||||
data.localUpdated.moveToNext();
|
||||
TYPE local = read(data.localUpdated);
|
||||
try {
|
||||
if(local.task == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there is a conflict, merge
|
||||
int remoteIndex = matchTask((ArrayList<TYPE>)data.remoteUpdated, local);
|
||||
if(remoteIndex != -1) {
|
||||
TYPE remote = data.remoteUpdated.get(remoteIndex);
|
||||
|
||||
remote = push(local, remote);
|
||||
|
||||
// re-read remote task after merge (with local's title)
|
||||
remote.task.setId(local.task.getId());
|
||||
data.remoteUpdated.set(remoteIndex, remote);
|
||||
} else {
|
||||
push(local, null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleException("sync-local-updated", e, false); //$NON-NLS-1$
|
||||
}
|
||||
write(local);
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendLocallyCreated(SyncData<TYPE> data,
|
||||
HashMap<String, Integer> remoteNewTaskNameMap) {
|
||||
int length;
|
||||
length = data.localCreated.getCount();
|
||||
for(int i = 0; i < length; i++) {
|
||||
data.localCreated.moveToNext();
|
||||
TYPE local = read(data.localCreated);
|
||||
try {
|
||||
|
||||
String taskTitle = local.task.getValue(Task.TITLE);
|
||||
|
||||
/* If there exists an incoming remote task with the same name and no
|
||||
* mapping, we don't want to create this on the remote server,
|
||||
* because user could have synchronized this before. Instead,
|
||||
* we create a mapping and do an update.
|
||||
*/
|
||||
if (remoteNewTaskNameMap.containsKey(taskTitle)) {
|
||||
int remoteIndex = remoteNewTaskNameMap.remove(taskTitle);
|
||||
TYPE remote = data.remoteUpdated.get(remoteIndex);
|
||||
|
||||
transferIdentifiers(remote, local);
|
||||
remote = push(local, remote);
|
||||
|
||||
// re-read remote task after merge, update remote task list
|
||||
remote.task.setId(local.task.getId());
|
||||
data.remoteUpdated.set(remoteIndex, remote);
|
||||
|
||||
} else {
|
||||
create(local);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleException("sync-local-created", e, false); //$NON-NLS-1$
|
||||
}
|
||||
write(local);
|
||||
}
|
||||
}
|
||||
|
||||
// --- exception handling
|
||||
|
||||
/**
|
||||
* Deal with a synchronization exception. If requested, will show an error
|
||||
* to the user (unless synchronization is happening in background)
|
||||
*
|
||||
* @param tag
|
||||
* error tag
|
||||
* @param e
|
||||
* exception
|
||||
*/
|
||||
protected void handleException(String tag, Exception e, boolean displayError) {
|
||||
final Context context = ContextManager.getContext();
|
||||
getUtilities().setLastError(e.toString(), "");
|
||||
|
||||
String message = null;
|
||||
|
||||
// occurs when application was closed
|
||||
if(e instanceof IllegalStateException) {
|
||||
exceptionService.reportError(tag + "-caught", e); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
// occurs when network error
|
||||
else if(e instanceof IOException) {
|
||||
exceptionService.reportError(tag + "-io", e); //$NON-NLS-1$
|
||||
message = context.getString(R.string.SyP_ioerror);
|
||||
}
|
||||
|
||||
// unhandled error
|
||||
else {
|
||||
message = context.getString(R.string.DLG_error, e.toString());
|
||||
exceptionService.reportError(tag + "-unhandled", e); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
if(displayError && context instanceof Activity && message != null) {
|
||||
DialogUtilities.okDialog((Activity)context,
|
||||
message, null);
|
||||
}
|
||||
}
|
||||
|
||||
// --- helper classes
|
||||
|
||||
/** data structure builder */
|
||||
protected static class SyncData<TYPE extends SyncContainer> {
|
||||
public ArrayList<TYPE> remoteUpdated;
|
||||
|
||||
public TodorooCursor<Task> localCreated;
|
||||
public TodorooCursor<Task> localUpdated;
|
||||
|
||||
public SyncData(ArrayList<TYPE> remoteUpdated,
|
||||
TodorooCursor<Task> localCreated,
|
||||
TodorooCursor<Task> localUpdated) {
|
||||
super();
|
||||
this.remoteUpdated = remoteUpdated;
|
||||
this.localCreated = localCreated;
|
||||
this.localUpdated = localUpdated;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue