diff --git a/.gitignore b/.gitignore index 7625933ac..799d44a18 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ release dev lp-translations/ translations/strings.xml - +.DS_Store +greendroid/GDCatalog/.project diff --git a/api/res/drawable/image_placeholder.png b/api/res/drawable/image_placeholder.png new file mode 100644 index 000000000..34ae5497d Binary files /dev/null and b/api/res/drawable/image_placeholder.png differ diff --git a/api/res/values/strings.xml b/api/res/values/strings.xml index f9afe8555..90bc156d3 100644 --- a/api/res/values/strings.xml +++ b/api/res/values/strings.xml @@ -76,6 +76,12 @@ %d tasks + + + 1 person + + %d people + diff --git a/api/src/com/todoroo/andlib/data/AbstractDatabase.java b/api/src/com/todoroo/andlib/data/AbstractDatabase.java index 398062d02..1e5479736 100644 --- a/api/src/com/todoroo/andlib/data/AbstractDatabase.java +++ b/api/src/com/todoroo/andlib/data/AbstractDatabase.java @@ -5,6 +5,8 @@ */ package com.todoroo.andlib.data; +import java.util.ArrayList; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -82,6 +84,30 @@ abstract public class AbstractDatabase { */ protected SQLiteDatabase database = null; + // --- listeners + + /** + * Interface for responding to database changes + */ + public interface DatabaseUpdateListener { + /** + * Called when an INSERT, UPDATE, or DELETE occurs + */ + public void onDatabaseUpdated(); + } + + private final ArrayList listeners = new ArrayList(); + + public void addListener(DatabaseUpdateListener listener) { + listeners.add(listener); + } + + protected void onDatabaseUpdated() { + for(DatabaseUpdateListener listener : listeners) { + listener.onDatabaseUpdated(); + } + } + // --- internal implementation @Autowired @@ -204,21 +230,27 @@ abstract public class AbstractDatabase { * @see android.database.sqlite.SQLiteDatabase#insert(String table, String nullColumnHack, ContentValues values) */ public synchronized long insert(String table, String nullColumnHack, ContentValues values) { - return getDatabase().insert(table, nullColumnHack, values); + long result = getDatabase().insert(table, nullColumnHack, values); + onDatabaseUpdated(); + return result; } /* * @see android.database.sqlite.SQLiteDatabase#delete(String table, String whereClause, String[] whereArgs) */ public synchronized int delete(String table, String whereClause, String[] whereArgs) { - return getDatabase().delete(table, whereClause, whereArgs); + int result = getDatabase().delete(table, whereClause, whereArgs); + onDatabaseUpdated(); + return result; } /* * @see android.database.sqlite.SQLiteDatabase#update(String table, ContentValues values, String whereClause, String[] whereArgs) */ public synchronized int update(String table, ContentValues values, String whereClause, String[] whereArgs) { - return getDatabase().update(table, values, whereClause, whereArgs); + int result = getDatabase().update(table, values, whereClause, whereArgs); + onDatabaseUpdated(); + return result; } // --- helper classes diff --git a/api/src/com/todoroo/andlib/data/AbstractModel.java b/api/src/com/todoroo/andlib/data/AbstractModel.java index c833ec3f9..7f06a210e 100644 --- a/api/src/com/todoroo/andlib/data/AbstractModel.java +++ b/api/src/com/todoroo/andlib/data/AbstractModel.java @@ -30,7 +30,7 @@ import com.todoroo.andlib.data.Property.PropertyVisitor; * @author Tim Su * */ -public abstract class AbstractModel implements Parcelable { +public abstract class AbstractModel implements Parcelable, Cloneable { // --- static variables @@ -129,6 +129,21 @@ public abstract class AbstractModel implements Parcelable { return getMergedValues().hashCode() ^ getClass().hashCode(); } + @Override + public AbstractModel clone() { + AbstractModel clone; + try { + clone = (AbstractModel) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + if(setValues != null) + clone.setValues = new ContentValues(setValues); + if(values != null) + clone.values = new ContentValues(values); + return clone; + } + // --- data retrieval /** @@ -201,7 +216,7 @@ public abstract class AbstractModel implements Parcelable { setValues = new ContentValues(); if(id == NO_ID) - setValues.remove(ID_PROPERTY_NAME); + clearValue(ID_PROPERTY); else setValues.put(ID_PROPERTY_NAME, id); } @@ -294,10 +309,30 @@ public abstract class AbstractModel implements Parcelable { public synchronized void clearValue(Property property) { if(setValues != null && setValues.containsKey(property.name)) setValues.remove(property.name); - else if(values != null && values.containsKey(property.name)) + if(values != null && values.containsKey(property.name)) values.remove(property.name); - else if(getDefaultValues().containsKey(property.name)) - throw new IllegalArgumentException("Property has a default value"); //$NON-NLS-1$ + } + + /** + * Sets the state of the given flag on the given property + * @param property + * @param flag + * @param value + */ + public void setFlag(IntegerProperty property, int flag, boolean value) { + if(value) + setValue(property, getValue(property) | flag); + else + setValue(property, getValue(property) & ~flag); + } + + /** + * Gets the state of the given flag on the given property + * @param property + * @param flag + */ + public boolean getFlag(IntegerProperty property, int flag) { + return (getValue(property) & flag) > 0; } // --- property management @@ -318,6 +353,8 @@ public abstract class AbstractModel implements Parcelable { if(!Property.class.isAssignableFrom(field.getType())) continue; try { + if(((Property) field.get(null)).table == null) + continue; properties.add((Property) field.get(null)); } catch (IllegalArgumentException e) { throw new RuntimeException(e); diff --git a/api/src/com/todoroo/andlib/data/DatabaseDao.java b/api/src/com/todoroo/andlib/data/DatabaseDao.java index ac8fc3d25..8ee776d7f 100644 --- a/api/src/com/todoroo/andlib/data/DatabaseDao.java +++ b/api/src/com/todoroo/andlib/data/DatabaseDao.java @@ -7,6 +7,7 @@ package com.todoroo.andlib.data; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; import android.content.ContentValues; import android.database.Cursor; @@ -68,6 +69,25 @@ public class DatabaseDao { table = database.getTable(modelClass); } + // --- listeners + + public interface ModelUpdateListener { + public void onModelUpdated(MTYPE model); + } + + private final ArrayList> listeners = + new ArrayList>(); + + public void addListener(ModelUpdateListener listener) { + listeners.add(listener); + } + + protected void onModelUpdated(TYPE model) { + for(ModelUpdateListener listener : listeners) { + listener.onModelUpdated(model); + } + } + // --- dao methods /** @@ -207,12 +227,14 @@ public class DatabaseDao { * @return returns true on success. */ public boolean createNew(TYPE item) { + item.clearValue(AbstractModel.ID_PROPERTY); long newRow = database.insert(table.name, AbstractModel.ID_PROPERTY.name, item.getMergedValues()); boolean result = newRow >= 0; if(result) { - item.markSaved(); item.setId(newRow); + onModelUpdated(item); + item.markSaved(); } return result; } @@ -233,8 +255,10 @@ public class DatabaseDao { return true; boolean result = database.update(table.name, values, AbstractModel.ID_PROPERTY.eq(item.getId()).toString(), null) > 0; - if(result) + if(result) { + onModelUpdated(item); item.markSaved(); + } return result; } diff --git a/api/src/com/todoroo/andlib/service/HttpRestClient.java b/api/src/com/todoroo/andlib/service/HttpRestClient.java index 15aedf6b7..cc3f4e37e 100644 --- a/api/src/com/todoroo/andlib/service/HttpRestClient.java +++ b/api/src/com/todoroo/andlib/service/HttpRestClient.java @@ -11,7 +11,6 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; @@ -142,13 +141,13 @@ public class HttpRestClient implements RestClient { * url-encoded data * @throws IOException */ - public synchronized String post(String url, String data) throws IOException { + public synchronized String post(String url, HttpEntity data) throws IOException { if(debug) Log.d("http-rest-client-post", url + " | " + data); //$NON-NLS-1$ //$NON-NLS-2$ try { HttpPost httpPost = new HttpPost(url); - httpPost.setEntity(new StringEntity(data)); + httpPost.setEntity(data); HttpResponse response = getClient().execute(httpPost); return processHttpResponse(response); diff --git a/api/src/com/todoroo/andlib/service/RestClient.java b/api/src/com/todoroo/andlib/service/RestClient.java index 0bf35e461..b2a990bb5 100644 --- a/api/src/com/todoroo/andlib/service/RestClient.java +++ b/api/src/com/todoroo/andlib/service/RestClient.java @@ -2,6 +2,8 @@ package com.todoroo.andlib.service; import java.io.IOException; +import org.apache.http.HttpEntity; + /** * RestClient stub invokes the HTML requests as desired * @@ -10,5 +12,5 @@ import java.io.IOException; */ public interface RestClient { public String get(String url) throws IOException; - public String post(String url, String data) throws IOException; + public String post(String url, HttpEntity data) throws IOException; } \ No newline at end of file diff --git a/api/src/com/todoroo/andlib/sql/Field.java b/api/src/com/todoroo/andlib/sql/Field.java index d0aaf2fe3..809cce160 100644 --- a/api/src/com/todoroo/andlib/sql/Field.java +++ b/api/src/com/todoroo/andlib/sql/Field.java @@ -83,7 +83,7 @@ public class Field extends DBObject { return UnaryCriterion.like(this, value, escape); } - public Criterion in(final T... value) { + public Criterion in(final T[] value) { final Field field = this; return new Criterion(Operator.in) { diff --git a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java index bcf38b7c3..838d32200 100644 --- a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java @@ -8,6 +8,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; @@ -300,11 +301,7 @@ public class AndroidUtilities { FileInputStream fis = new FileInputStream(in); FileOutputStream fos = new FileOutputStream(out); try { - byte[] buf = new byte[1024]; - int i = 0; - while ((i = fis.read(buf)) != -1) { - fos.write(buf, 0, i); - } + copyStream(fis, fos); } catch (Exception e) { throw e; } finally { @@ -313,6 +310,32 @@ public class AndroidUtilities { } } + /** + * Copy stream from source to destination + * @param source + * @param dest + * @throws IOException + */ + public static void copyStream(InputStream source, OutputStream dest) throws IOException { + int bytes; + byte[] buffer; + int BUFFER_SIZE = 1024; + buffer = new byte[BUFFER_SIZE]; + while ((bytes = source.read(buffer)) != -1) { + if (bytes == 0) { + bytes = source.read(); + if (bytes < 0) + break; + dest.write(bytes); + dest.flush(); + continue; + } + + dest.write(buffer, 0, bytes); + dest.flush(); + } + } + /** * Find a child view of a certain type * @param view @@ -544,4 +567,20 @@ public class AndroidUtilities { return exceptionService; } + /** + * Concatenate additional stuff to the end of the array + * @param params + * @param additional + * @return + */ + public static TYPE[] concat(TYPE[] dest, TYPE[] source, TYPE... additional) { + int i = 0; + for(; i < Math.min(dest.length, source.length); i++) + dest[i] = source[i]; + int base = i; + for(; i < dest.length; i++) + dest[i] = additional[i - base]; + return dest; + } + } diff --git a/api/src/com/todoroo/andlib/utility/ImageLoader.java b/api/src/com/todoroo/andlib/utility/ImageLoader.java new file mode 100644 index 000000000..65f4380ab --- /dev/null +++ b/api/src/com/todoroo/andlib/utility/ImageLoader.java @@ -0,0 +1,224 @@ +package com.todoroo.andlib.utility; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Stack; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; +import android.widget.ImageView; + +import com.todoroo.astrid.api.R; + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. + */ + +public class ImageLoader { + + // the simplest in-memory cache implementation. This should be replaced with + // something like SoftReference or BitmapOptions.inPurgeable(since 1.6) + private final HashMap cache = new HashMap(); + + private File cacheDir; + + public ImageLoader(Context context) { + // Make the background thread low priority. This way it will not affect + // the UI performance + photoLoaderThread.setPriority(Thread.NORM_PRIORITY - 1); + + // Find the dir to save cached images + if (android.os.Environment.getExternalStorageState().equals( + android.os.Environment.MEDIA_MOUNTED)) + cacheDir = new File( + android.os.Environment.getExternalStorageDirectory(), + "Android/data/com.todoroo.astrid/cache/"); //$NON-NLS-1$ + else + cacheDir = context.getCacheDir(); + if (!cacheDir.exists()) + cacheDir.mkdirs(); + } + + final int stub_id = R.drawable.image_placeholder; + + public void displayImage(String url, ImageView imageView) { + if (cache.containsKey(url)) + imageView.setImageURI(cache.get(url)); + else { + queuePhoto(url, imageView); + imageView.setImageResource(stub_id); + } + } + + private void queuePhoto(String url, ImageView imageView) { + // This ImageView may be used for other images before. So there may be + // some old tasks in the queue. We need to discard them. + photosQueue.Clean(imageView); + PhotoToLoad p = new PhotoToLoad(url, imageView); + synchronized (photosQueue.photosToLoad) { + photosQueue.photosToLoad.push(p); + photosQueue.photosToLoad.notifyAll(); + } + + // start thread if it's not started yet + if (photoLoaderThread.getState() == Thread.State.NEW) + photoLoaderThread.start(); + } + + private Uri getUri(String url) { + if(!TextUtils.isEmpty(url) && url.contains("://")) { //$NON-NLS-1$ + // identify images by hashcode. Not a perfect solution. + String filename = String.valueOf(url.hashCode()); + File f = new File(cacheDir, filename); + + // from SD cache + if (f.exists()) { + Uri b = Uri.fromFile(f); + System.out.println(f.toString()); + return b; + } + + // from web + try { + Uri bitmap = null; + InputStream is = new URL(url).openStream(); + OutputStream os = new FileOutputStream(f); + AndroidUtilities.copyStream(is, os); + os.close(); + bitmap = Uri.fromFile(f); + System.out.println(f.toString()); + return bitmap; + } catch (Exception e) { + Log.e("imagel-loader", "Unable to get URL", e); //$NON-NLS-1$ //$NON-NLS-2$ + return null; + } + } else { + return null; + } + } + + // Task for the queue + private class PhotoToLoad { + public String url; + public ImageView imageView; + + public PhotoToLoad(String u, ImageView i) { + url = u; + imageView = i; + imageView.setTag(url); + } + } + + PhotosQueue photosQueue = new PhotosQueue(); + + public void stopThread() { + photoLoaderThread.interrupt(); + } + + // stores list of photos to download + class PhotosQueue { + private final Stack photosToLoad = new Stack(); + + // removes all instances of this ImageView + public void Clean(ImageView image) { + for (int j = 0; j < photosToLoad.size();) { + if (photosToLoad.get(j).imageView == image) + photosToLoad.remove(j); + else + ++j; + } + } + } + + class PhotosLoader extends Thread { + @Override + public void run() { + try { + while (true) { + // thread waits until there are any images to load in the + // queue + if (photosQueue.photosToLoad.size() == 0) + synchronized (photosQueue.photosToLoad) { + photosQueue.photosToLoad.wait(); + } + if (photosQueue.photosToLoad.size() != 0) { + PhotoToLoad photoToLoad; + synchronized (photosQueue.photosToLoad) { + photoToLoad = photosQueue.photosToLoad.pop(); + } + Uri bmp = getUri(photoToLoad.url); + cache.put(photoToLoad.url, bmp); + if (((String) photoToLoad.imageView.getTag()).equals(photoToLoad.url)) { + UriDisplayer bd = new UriDisplayer(bmp, + photoToLoad.imageView); + Activity a = (Activity) photoToLoad.imageView.getContext(); + a.runOnUiThread(bd); + } + } + if (Thread.interrupted()) + break; + } + } catch (InterruptedException e) { + // allow thread to exit + } + } + } + + PhotosLoader photoLoaderThread = new PhotosLoader(); + + class UriDisplayer implements Runnable { + Uri uri; + ImageView imageView; + + public UriDisplayer(Uri u, ImageView i) { + uri = u; + imageView = i; + } + + public void run() { + if(uri == null) + return; + + File f = new File(uri.getPath()); + if (f.exists()) { + imageView.setImageURI(Uri.parse(f.toString())); + } else { + imageView.setImageResource(stub_id); + } + + } + } + + public void clearCache() { + // clear memory cache + cache.clear(); + + // clear SD cache + File[] files = cacheDir.listFiles(); + for (File f : files) + f.delete(); + } + +} \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java b/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java index 70da0dc28..36d015652 100644 --- a/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java +++ b/api/src/com/todoroo/astrid/api/FilterWithCustomIntent.java @@ -11,7 +11,14 @@ import com.todoroo.andlib.sql.QueryTemplate; public class FilterWithCustomIntent extends Filter { + /** + * Custom activity name + */ public ComponentName customTaskList = null; + + /** + * Bundle with extras set. Can be null + */ public Bundle customExtras = null; protected FilterWithCustomIntent() { diff --git a/api/src/com/todoroo/astrid/data/RemoteModel.java b/api/src/com/todoroo/astrid/data/RemoteModel.java new file mode 100644 index 000000000..ea073b6ce --- /dev/null +++ b/api/src/com/todoroo/astrid/data/RemoteModel.java @@ -0,0 +1,33 @@ +package com.todoroo.astrid.data; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; + +/** + * A model that is synchronized to a remote server and has a remote id + * + * @author Tim Su + * + */ +abstract public class RemoteModel extends AbstractModel { + + /** remote id property common to all remote models */ + public static final String REMOTE_ID_PROPERTY_NAME = "remoteId"; //$NON-NLS-1$ + + /** remote id property */ + public static final LongProperty REMOTE_ID_PROPERTY = new LongProperty(null, REMOTE_ID_PROPERTY_NAME); + + /** user id property common to all remote models */ + protected static final String USER_ID_PROPERTY_NAME = "userId"; //$NON-NLS-1$ + + /** user id property */ + public static final LongProperty USER_ID_PROPERTY = new LongProperty(null, USER_ID_PROPERTY_NAME); + + /** user json property common to all remote models */ + protected static final String USER_JSON_PROPERTY_NAME = "user"; //$NON-NLS-1$ + + /** user json property */ + public static final StringProperty USER_JSON_PROPERTY = new StringProperty(null, USER_JSON_PROPERTY_NAME); + +} diff --git a/api/src/com/todoroo/astrid/data/TagData.java b/api/src/com/todoroo/astrid/data/TagData.java new file mode 100644 index 000000000..481df99f3 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/TagData.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.astrid.data; + + +import android.content.ContentValues; +import android.net.Uri; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Property.IntegerProperty; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.astrid.api.AstridApiConstants; + +/** + * Data Model which represents a collaboration space for users / tasks. + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public final class TagData extends RemoteModel { + + // --- table and uri + + /** table for this model */ + public static final Table TABLE = new Table("tagdata", TagData.class); + + /** content uri for this model */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.PACKAGE + "/" + + TABLE.name); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** User id */ + public static final LongProperty USER_ID = new LongProperty( + TABLE, USER_ID_PROPERTY_NAME); + + /** User Object (JSON) */ + public static final StringProperty USER = new StringProperty( + TABLE, USER_JSON_PROPERTY_NAME); + + /** Remote goal id */ + public static final LongProperty REMOTE_ID = new LongProperty( + TABLE, REMOTE_ID_PROPERTY_NAME); + + /** Name of Tag */ + public static final StringProperty NAME = new StringProperty( + TABLE, "name"); + + /** Project picture */ + public static final StringProperty PICTURE = new StringProperty( + TABLE, "picture"); + + /** Tag team array (JSON) */ + public static final StringProperty MEMBERS = new StringProperty( + TABLE, "members"); + + /** Tag member count */ + public static final IntegerProperty MEMBER_COUNT = new IntegerProperty( + TABLE, "memberCount"); + + /** Flags */ + public static final IntegerProperty FLAGS = new IntegerProperty( + TABLE, "flags"); + + /** Unixtime Project was created */ + public static final LongProperty CREATION_DATE = new LongProperty( + TABLE, "created"); + + /** Unixtime Project was last touched */ + public static final LongProperty MODIFICATION_DATE = new LongProperty( + TABLE, "modified"); + + /** Unixtime Project was completed. 0 means active */ + public static final LongProperty COMPLETION_DATE = new LongProperty( + TABLE, "completed"); + + /** Unixtime Project was deleted. 0 means not deleted */ + public static final LongProperty DELETION_DATE = new LongProperty( + TABLE, "deleted"); + + /** Project picture thumbnail */ + public static final StringProperty THUMB = new StringProperty( + TABLE, "thumb"); + + /** Project last activity date */ + public static final LongProperty LAST_ACTIVITY_DATE = new LongProperty( + TABLE, "lastActivityDate"); + + /** Whether user is part of Tag team */ + public static final IntegerProperty IS_TEAM = new IntegerProperty( + TABLE, "isTeam"); + + /** Whether Tag has unread activity */ + public static final IntegerProperty IS_UNREAD = new IntegerProperty( + TABLE, "isUnread"); + + /** Task count */ + public static final IntegerProperty TASK_COUNT = new IntegerProperty( + TABLE, "taskCount"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(TagData.class); + + // --- flags + + /** whether tag is publicly visible */ + public static final int FLAG_PUBLIC = 1 << 0; + + /** whether user should not be notified of tag activity */ + public static final int FLAG_SILENT = 1 << 1; + + /** whether tag is emergent */ + public static final int FLAG_EMERGENT = 1 << 2; + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + static { + defaultValues.put(USER_ID.name, 0); + defaultValues.put(USER.name, "{}"); + defaultValues.put(REMOTE_ID.name, 0); + defaultValues.put(NAME.name, ""); + defaultValues.put(PICTURE.name, ""); + defaultValues.put(IS_TEAM.name, 1); + defaultValues.put(MEMBERS.name, "[]"); + defaultValues.put(MEMBER_COUNT.name, 0); + defaultValues.put(FLAGS.name, 0); + defaultValues.put(COMPLETION_DATE.name, 0); + defaultValues.put(DELETION_DATE.name, 0); + + defaultValues.put(THUMB.name, ""); + defaultValues.put(LAST_ACTIVITY_DATE.name, 0); + defaultValues.put(IS_UNREAD.name, 0); + defaultValues.put(TASK_COUNT.name, 0); + } + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public TagData() { + super(); + } + + public TagData(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + } + + // --- parcelable helpers + + public static final Creator CREATOR = new ModelCreator(TagData.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + + // --- data access methods + + /** Checks whether task is done. Requires COMPLETION_DATE */ + public boolean isCompleted() { + return getValue(COMPLETION_DATE) > 0; + } + + /** Checks whether task is deleted. Will return false if DELETION_DATE not read */ + public boolean isDeleted() { + // assume false if we didn't load deletion date + if(!containsValue(DELETION_DATE)) + return false; + else + return getValue(DELETION_DATE) > 0; + } + +} diff --git a/api/src/com/todoroo/astrid/data/Task.java b/api/src/com/todoroo/astrid/data/Task.java index f0b31fda3..7388c2099 100644 --- a/api/src/com/todoroo/astrid/data/Task.java +++ b/api/src/com/todoroo/astrid/data/Task.java @@ -30,7 +30,7 @@ import com.todoroo.astrid.api.R; * */ @SuppressWarnings("nls") -public final class Task extends AbstractModel { +public final class Task extends RemoteModel { // --- table and uri @@ -89,8 +89,10 @@ public final class Task extends AbstractModel { public static final LongProperty DETAILS_DATE = new LongProperty( TABLE, "detailsDate"); - // --- for migration purposes from astrid 2 (eventually we may want to - // move these into the metadata table and treat them as plug-ins + public static final IntegerProperty FLAGS = new IntegerProperty( + TABLE, "flags"); + + // --- non-core task metadata public static final StringProperty NOTES = new StringProperty( TABLE, "notes"); @@ -126,12 +128,38 @@ public final class Task extends AbstractModel { public static final StringProperty RECURRENCE = new StringProperty( TABLE, "recurrence"); - public static final IntegerProperty FLAGS = new IntegerProperty( - TABLE, "flags"); - public static final StringProperty CALENDAR_URI = new StringProperty( TABLE, "calendarUri"); + // --- for astrid.com + + /** Remote id */ + public static final LongProperty REMOTE_ID = new LongProperty( + TABLE, REMOTE_ID_PROPERTY_NAME); + + /** Assigned user id */ + public static final LongProperty USER_ID = new LongProperty( + TABLE, USER_ID_PROPERTY_NAME); + + /** User Object (JSON) */ + public static final StringProperty USER = new StringProperty( + TABLE, USER_JSON_PROPERTY_NAME); + + /** Creator user id */ + public static final LongProperty CREATOR_ID = new LongProperty( + TABLE, "creatorId"); + + public static final StringProperty SHARED_WITH = new StringProperty( + TABLE, "sharedWith"); + + /** Comment Count */ + public static final IntegerProperty COMMENT_COUNT = new IntegerProperty( + TABLE, "commentCount"); + + /** Last Sync date */ + public static final LongProperty LAST_SYNC = new LongProperty( + TABLE, "lastSync"); + /** List of all properties for this model */ public static final Property[] PROPERTIES = generateProperties(Task.class); @@ -208,6 +236,12 @@ public final class Task extends AbstractModel { defaultValues.put(TIMER_START.name, 0); defaultValues.put(DETAILS.name, (String)null); defaultValues.put(DETAILS_DATE.name, 0); + + defaultValues.put(LAST_SYNC.name, 0); + defaultValues.put(REMOTE_ID.name, 0); + defaultValues.put(USER_ID.name, 0); + defaultValues.put(USER.name, ""); + defaultValues.put(SHARED_WITH.name, ""); } @Override @@ -276,23 +310,11 @@ public final class Task extends AbstractModel { * @param flag * @return */ + @Override public boolean getFlag(IntegerProperty property, int flag) { return (getValue(property) & flag) > 0; } - /** - * Sets the state of the given flag on the given property - * @param property - * @param flag - * @param value - */ - public void setFlag(IntegerProperty property, int flag, boolean value) { - if(value) - setValue(property, getValue(property) | flag); - else - setValue(property, getValue(property) & ~flag); - } - // --- due and hide until date management /** urgency array index -> significance */ diff --git a/api/src/com/todoroo/astrid/data/Update.java b/api/src/com/todoroo/astrid/data/Update.java new file mode 100644 index 000000000..fc9de1122 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/Update.java @@ -0,0 +1,141 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.data; + + +import android.content.ContentValues; +import android.net.Uri; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.astrid.api.AstridApiConstants; + +/** + * Data Model which represents an update (e.g. a comment or data update event) + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public class Update extends RemoteModel { + + // --- table + + /** table for this model */ + public static final Table TABLE = new Table("updates", Update.class); + + /** content uri for this model */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.PACKAGE + "/" + + TABLE.name); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** Remote ID */ + public static final LongProperty REMOTE_ID = new LongProperty( + TABLE, REMOTE_ID_PROPERTY_NAME); + + /** Associated Task (if any) */ + public static final LongProperty TASK = new LongProperty( + TABLE, "task"); + + /** Associated Project (if any) */ + public static final LongProperty TAG = new LongProperty( + TABLE, "tag"); + + /** From user id */ + public static final LongProperty USER_ID = new LongProperty( + TABLE, USER_ID_PROPERTY_NAME); + + /** From User Object (JSON) */ + public static final StringProperty USER = new StringProperty( + TABLE, USER_JSON_PROPERTY_NAME); + + /** Action text */ + public static final StringProperty ACTION = new StringProperty( + TABLE, "action"); + + /** Action code */ + public static final StringProperty ACTION_CODE = new StringProperty( + TABLE, "actionCode"); + + /** Message */ + public static final StringProperty MESSAGE = new StringProperty( + TABLE, "message"); + + /** Target Object Name */ + public static final StringProperty TARGET_NAME = new StringProperty( + TABLE, "targetName"); + + /** From User Object (JSON) */ + public static final StringProperty PICTURE = new StringProperty( + TABLE, "picture"); + + /** Unixtime Metadata was created */ + public static final LongProperty CREATION_DATE = new LongProperty( + TABLE, "created"); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(Update.class); + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + static { + defaultValues.put(REMOTE_ID.name, 0); + defaultValues.put(TASK.name, 0); + defaultValues.put(TAG.name, 0); + defaultValues.put(USER_ID.name, 0); + defaultValues.put(USER.name, ""); + defaultValues.put(ACTION.name, ""); + defaultValues.put(ACTION_CODE.name, ""); + defaultValues.put(MESSAGE.name, ""); + defaultValues.put(TARGET_NAME.name, ""); + defaultValues.put(PICTURE.name, ""); + } + + // --- data access boilerplate + + public Update() { + super(); + } + + public Update(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + }; + + // --- parcelable helpers + + private static final Creator CREATOR = new ModelCreator(Update.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + +} diff --git a/api/src/com/todoroo/astrid/data/User.java b/api/src/com/todoroo/astrid/data/User.java new file mode 100644 index 000000000..308f94e38 --- /dev/null +++ b/api/src/com/todoroo/astrid/data/User.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2009, Todoroo Inc + * All Rights Reserved + * http://www.todoroo.com + */ +package com.todoroo.astrid.data; + + +import android.content.ContentValues; +import android.net.Uri; + +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.andlib.data.Table; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.astrid.api.AstridApiConstants; + +/** + * Data Model which represents a user. + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public final class User extends RemoteModel { + + // --- table and uri + + /** table for this model */ + public static final Table TABLE = new Table("users", User.class); + + /** content uri for this model */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.PACKAGE + "/" + + TABLE.name); + + // --- properties + + /** ID */ + public static final LongProperty ID = new LongProperty( + TABLE, ID_PROPERTY_NAME); + + /** User Name */ + public static final StringProperty NAME = new StringProperty( + TABLE, "name"); + + /** User Email */ + public static final StringProperty EMAIL = new StringProperty( + TABLE, "email"); + + /** User picture */ + public static final StringProperty PICTURE = new StringProperty( + TABLE, "picture"); + + /** User picture thumbnail */ + public static final StringProperty THUMB = new StringProperty( + TABLE, "thumb"); + + /** User last activity string */ + public static final StringProperty LAST_ACTIVITY = new StringProperty( + TABLE, "lastActivity"); + + /** User last activity date */ + public static final LongProperty LAST_ACTIVITY_DATE = new LongProperty( + TABLE, "lastActivityDate"); + + /** Remote id */ + public static final LongProperty REMOTE_ID = new LongProperty( + TABLE, REMOTE_ID_PROPERTY_NAME); + + /** List of all properties for this model */ + public static final Property[] PROPERTIES = generateProperties(User.class); + + // --- defaults + + /** Default values container */ + private static final ContentValues defaultValues = new ContentValues(); + + static { + defaultValues.put(NAME.name, ""); + defaultValues.put(EMAIL.name, ""); + defaultValues.put(PICTURE.name, ""); + defaultValues.put(THUMB.name, ""); + defaultValues.put(LAST_ACTIVITY.name, ""); + defaultValues.put(LAST_ACTIVITY_DATE.name, 0); + } + + @Override + public ContentValues getDefaultValues() { + return defaultValues; + } + + // --- data access boilerplate + + public User() { + super(); + } + + public User(TodorooCursor cursor) { + this(); + readPropertiesFromCursor(cursor); + } + + public void readFromCursor(TodorooCursor cursor) { + super.readPropertiesFromCursor(cursor); + } + + @Override + public long getId() { + return getIdHelper(ID); + } + + // --- parcelable helpers + + public static final Creator CREATOR = new ModelCreator(User.class); + + @Override + protected Creator getCreator() { + return CREATOR; + } + +} diff --git a/api/src/com/todoroo/astrid/sync/SyncProvider.java b/api/src/com/todoroo/astrid/sync/SyncProvider.java index 6e037d6f4..ceb71626f 100644 --- a/api/src/com/todoroo/astrid/sync/SyncProvider.java +++ b/api/src/com/todoroo/astrid/sync/SyncProvider.java @@ -17,7 +17,12 @@ 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.api.R; import com.todoroo.astrid.data.Task; @@ -40,6 +45,11 @@ public abstract class SyncProvider { // --- 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 @@ -65,36 +75,25 @@ public abstract class SyncProvider { */ abstract protected int updateNotification(Context context, Notification n); - /** - * Deal with an exception that occurs during synchronization - * - * @param tag - * short string description of where error occurred - * @param e - * exception - * @param displayError - * whether to display error to the user - */ - abstract protected void handleException(String tag, Exception e, boolean displayError); - /** * Create a task on the remote server. * * @param task * task to create - * @return task created on remote server */ abstract protected TYPE create(TYPE task) throws IOException; /** - * Push variables from given task to the remote server. + * Push variables from given task to the remote server, and read the newly + * updated task. * * @param task * task proxy to push * @param remoteTask * remote task that we merged with. may be null + * @return task pulled on remote server */ - abstract protected void push(TYPE task, TYPE remote) throws IOException; + abstract protected TYPE push(TYPE task, TYPE remote) throws IOException; /** * Fetch remote task. Used to re-read merged tasks @@ -138,7 +137,11 @@ public abstract class SyncProvider { 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(); @@ -274,10 +277,10 @@ public abstract class SyncProvider { int remoteIndex = matchTask((ArrayList)data.remoteUpdated, local); if(remoteIndex != -1) { TYPE remote = data.remoteUpdated.get(remoteIndex); - push(local, remote); + + remote = push(local, remote); // re-read remote task after merge (with local's title) - remote = pull(local); remote.task.setId(local.task.getId()); data.remoteUpdated.set(remoteIndex, remote); } else { @@ -311,10 +314,9 @@ public abstract class SyncProvider { TYPE remote = data.remoteUpdated.get(remoteIndex); transferIdentifiers(remote, local); - push(local, remote); + remote = push(local, remote); // re-read remote task after merge, update remote task list - remote = pull(remote); remote.task.setId(local.task.getId()); data.remoteUpdated.set(remoteIndex, remote); @@ -328,6 +330,49 @@ public abstract class SyncProvider { } } + // --- exception handling + + /** + * Deal with a synchronization exception. If requested, will show an error + * to the user (unless synchronization is happening in background) + * + * @param context + * @param tag + * error tag + * @param e + * exception + * @param showError + * whether to display a dialog + */ + 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 */ diff --git a/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java b/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java index 893729215..adae98072 100644 --- a/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java +++ b/api/src/com/todoroo/astrid/sync/SyncProviderPreferences.java @@ -12,6 +12,7 @@ import android.preference.Preference.OnPreferenceClickListener; import android.view.View; import android.view.ViewGroup.OnHierarchyChangeListener; +import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DialogUtilities; @@ -55,6 +56,10 @@ abstract public class SyncProviderPreferences extends TodorooPreferenceActivity private int statusColor = Color.BLACK; + public SyncProviderPreferences() { + DependencyInjectionService.getInstance().inject(this); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java b/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java index 0f43d9d2b..6624f9cca 100644 --- a/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java +++ b/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java @@ -102,7 +102,7 @@ abstract public class SyncProviderUtilities { /** Set Last Successful Sync Date */ public void recordSuccessfulSync() { Editor editor = getPrefs().edit(); - editor.putLong(getIdentifier() + PREF_LAST_SYNC, DateUtilities.now()); + editor.putLong(getIdentifier() + PREF_LAST_SYNC, DateUtilities.now() + 1000); editor.putLong(getIdentifier() + PREF_LAST_ATTEMPTED_SYNC, 0); editor.commit(); } diff --git a/astrid/.classpath b/astrid/.classpath index 3e5239912..b1900ed7a 100644 --- a/astrid/.classpath +++ b/astrid/.classpath @@ -3,7 +3,7 @@ - + @@ -15,6 +15,7 @@ + diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml index 0de0ecf9d..10210c842 100644 --- a/astrid/AndroidManifest.xml +++ b/astrid/AndroidManifest.xml @@ -25,6 +25,10 @@ + + + + @@ -49,6 +53,10 @@ android:protectionLevel="normal" android:label="@string/write_permission_label" /> + + + @@ -57,7 +65,8 @@ + android:theme="@style/Theme" + android:name="greendroid.app.GDApplication"> @@ -137,7 +146,7 @@ - + @@ -386,7 +395,63 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/astrid/antlib/debug.keystore b/astrid/antlib/debug.keystore new file mode 100644 index 000000000..e1af6062e Binary files /dev/null and b/astrid/antlib/debug.keystore differ diff --git a/astrid/astrid.launch b/astrid/astrid.launch index fc989823f..040b4cc52 100644 --- a/astrid/astrid.launch +++ b/astrid/astrid.launch @@ -6,8 +6,7 @@ - - + @@ -16,9 +15,11 @@ + + diff --git a/astrid/build.properties b/astrid/build.properties index 252d7f977..3e180a62a 100644 --- a/astrid/build.properties +++ b/astrid/build.properties @@ -14,3 +14,6 @@ astrid.sources=src,common-src,plugin-src,src-legacy,rmilk-src signjar.keystore=/etc/todoroo/keystore signjar.keyalias=anddev.keystore signjar.passfile=/etc/todoroo/keypass + +# Default API Keys +apikey.keyfile=/etc/todoroo/apikeys diff --git a/astrid/build.xml b/astrid/build.xml index 62bbb8a6a..bc4effccc 100644 --- a/astrid/build.xml +++ b/astrid/build.xml @@ -111,7 +111,7 @@ - + @@ -125,6 +125,23 @@ Final Release Package: ${out.final.package} + + + + + + + + + diff --git a/astrid/common-src/com/facebook/android/LoginButton.java b/astrid/common-src/com/facebook/android/LoginButton.java index 8fdce811e..0c3f0cf12 100644 --- a/astrid/common-src/com/facebook/android/LoginButton.java +++ b/astrid/common-src/com/facebook/android/LoginButton.java @@ -18,15 +18,14 @@ package com.facebook.android; import android.app.Activity; import android.content.Context; -import android.graphics.Color; import android.os.Bundle; import android.util.AttributeSet; import android.view.View; -import android.widget.ImageButton; +import android.widget.Button; import com.facebook.android.Facebook.DialogListener; -public class LoginButton extends ImageButton { +public class LoginButton extends Button { private Facebook mFb; private AuthListener mListener; @@ -56,23 +55,14 @@ public class LoginButton extends ImageButton { mPermissions = permissions; mListener = listener; - setBackgroundColor(Color.TRANSPARENT); - setAdjustViewBounds(true); - drawableStateChanged(); - setOnClickListener(new ButtonOnClickListener()); } private final class ButtonOnClickListener implements OnClickListener { public void onClick(View arg0) { - if (mFb.isSessionValid()) { - AsyncFacebookRunner asyncRunner = new AsyncFacebookRunner(mFb); - asyncRunner.logout(getContext(), null); - } else { - mFb.authorize(mActivity, mPermissions, - new LoginDialogListener()); - } + mFb.authorize(mActivity, mPermissions, + new LoginDialogListener()); } } diff --git a/astrid/libs/httpmime-4.1.1.jar b/astrid/libs/httpmime-4.1.1.jar new file mode 100644 index 000000000..01af40b24 Binary files /dev/null and b/astrid/libs/httpmime-4.1.1.jar differ diff --git a/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java b/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java new file mode 100644 index 000000000..647acaaea --- /dev/null +++ b/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java @@ -0,0 +1,225 @@ +package com.timsu.astrid; + +import java.io.IOException; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +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.NotificationManager; +import com.todoroo.andlib.service.NotificationManager.AndroidNotificationManager; +import com.todoroo.andlib.sql.Query; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.actfm.TagViewActivity; +import com.todoroo.astrid.actfm.sync.ActFmSyncService; +import com.todoroo.astrid.activity.ShortcutActivity; +import com.todoroo.astrid.activity.TaskListActivity; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.FilterWithCustomIntent; +import com.todoroo.astrid.core.CoreFilterExposer; +import com.todoroo.astrid.dao.UpdateDao; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.data.Update; +import com.todoroo.astrid.reminders.Notifications; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.tags.TagFilterExposer; +import com.todoroo.astrid.utility.Constants; +import com.todoroo.astrid.utility.Flags; + +@SuppressWarnings("nls") +public class C2DMReceiver extends BroadcastReceiver { + + public static final String C2DM_SENDER = "c2dm@astrid.com"; //$NON-NLS-1$ + + private static final String PREF_REGISTRATION = "c2dm_key"; + + @Autowired ActFmSyncService actFmSyncService; + @Autowired TagDataService tagDataService; + @Autowired UpdateDao updateDao; + + @Override + public void onReceive(Context context, final Intent intent) { + ContextManager.setContext(context); + DependencyInjectionService.getInstance().inject(this); + if (intent.getAction().equals("com.google.android.c2dm.intent.REGISTRATION")) { + handleRegistration(intent); + } else if (intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")) { + new Thread(new Runnable() { + @Override + public void run() { + handleMessage(intent); + } + }).start(); + } + } + + /** Handle message. Run on separate thread. */ + private void handleMessage(Intent intent) { + String message = intent.getStringExtra("alert"); + Context context = ContextManager.getContext(); + + Intent notifyIntent = null; + int notifId; + + // fetch data + if(intent.hasExtra("tag_id")) { + notifyIntent = createTagIntent(context, intent); + notifId = (int) Long.parseLong(intent.getStringExtra("tag_id")); + } else { + notifId = Constants.NOTIFICATION_ACTFM; + } + + if(notifyIntent == null) + notifyIntent = ShortcutActivity.createIntent(CoreFilterExposer.buildInboxFilter(context.getResources())); + + notifyIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity(context, + Constants.NOTIFICATION_ACTFM, notifyIntent, 0); + + // create notification + NotificationManager nm = new AndroidNotificationManager(ContextManager.getContext()); + Notification notification = new Notification(R.drawable.notif_pink_alarm, + message, System.currentTimeMillis()); + String title; + if(intent.hasExtra("title")) + title = "Astrid: " + intent.getStringExtra("title"); + else + title = ContextManager.getString(R.string.app_name); + notification.setLatestEventInfo(ContextManager.getContext(), title, + message, pendingIntent); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + + boolean sounds = !"false".equals(intent.getStringExtra("sound")); + notification.defaults = 0; + if(sounds && !Notifications.isQuietHours()) { + notification.defaults |= Notification.DEFAULT_SOUND; + notification.defaults |= Notification.DEFAULT_VIBRATE; + } + + nm.notify(notifId, notification); + + if(intent.hasExtra("tag_id")) { + Intent broadcastIntent = new Intent(TagViewActivity.BROADCAST_TAG_ACTIVITY); + broadcastIntent.putExtras(intent); + ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } + } + + private Intent createTagIntent(final Context context, final Intent intent) { + TodorooCursor cursor = tagDataService.query( + Query.select(TagData.PROPERTIES).where(TagData.REMOTE_ID.eq( + intent.getStringExtra("tag_id")))); + try { + final TagData tagData = new TagData(); + if(cursor.getCount() == 0) { + tagData.setValue(TagData.NAME, intent.getStringExtra("title")); + tagData.setValue(TagData.REMOTE_ID, Long.parseLong(intent.getStringExtra("tag_id"))); + Flags.set(Flags.SUPPRESS_SYNC); + tagDataService.save(tagData); + + new Thread(new Runnable() { + @Override + public void run() { + try { + actFmSyncService.fetchTag(tagData); + } catch (IOException e) { + Log.e("c2dm-tag-rx", "io-exception", e); + } catch (JSONException e) { + Log.e("c2dm-tag-rx", "json-exception", e); + } + } + }).start(); + } else { + cursor.moveToNext(); + tagData.readFromCursor(cursor); + } + + FilterWithCustomIntent filter = (FilterWithCustomIntent)TagFilterExposer.filterFromTagData(context, tagData); + if(intent.hasExtra("activity_id")) { + filter.customExtras.putInt(TagViewActivity.EXTRA_START_TAB, 1); + + try { + Update update = new Update(); + update.setValue(Update.REMOTE_ID, Long.parseLong(intent.getStringExtra("activity_id"))); + update.setValue(Update.USER_ID, Long.parseLong(intent.getStringExtra("user_id"))); + JSONObject user = new JSONObject(); + user.put("id", update.getValue(Update.USER_ID)); + user.put("name", intent.getStringExtra("user_name")); + update.setValue(Update.USER, user.toString()); + update.setValue(Update.ACTION, "commented"); + update.setValue(Update.ACTION_CODE, "tag_comment"); + update.setValue(Update.TARGET_NAME, intent.getStringExtra("title")); + String message = intent.getStringExtra("alert"); + if(message.contains(":")) + message = message.substring(message.indexOf(':') + 2); + update.setValue(Update.MESSAGE, message); + update.setValue(Update.CREATION_DATE, DateUtilities.now()); + update.setValue(Update.TAG, tagData.getId()); + updateDao.createNew(update); + } catch (JSONException e) { + // + } + + } + + Intent launchIntent = new Intent(); + launchIntent.putExtra(TaskListActivity.TOKEN_FILTER, filter); + launchIntent.setComponent(filter.customTaskList); + launchIntent.putExtras(filter.customExtras); + + return launchIntent; + } finally { + cursor.close(); + } + } + + private void handleRegistration(Intent intent) { + String registration = intent.getStringExtra("registration_id"); + if (intent.getStringExtra("error") != null) { + Log.w("astrid-actfm", "error-c2dm: " + intent.getStringExtra("error")); + } else if (intent.getStringExtra("unregistered") != null) { + // un-registration done + } else if (registration != null) { + try { + DependencyInjectionService.getInstance().inject(this); + actFmSyncService.invoke("user_set_c2dm", "c2dm", registration); + Preferences.setString(PREF_REGISTRATION, registration); + } catch (IOException e) { + Log.e("astrid-actfm", "error-c2dm-transfer", e); + } + } + } + + /** try to request registration from c2dm service */ + public static void register() { + if(Preferences.getStringValue(PREF_REGISTRATION) != null) + return; + + Context context = ContextManager.getContext(); + Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER"); + registrationIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0)); // boilerplate + registrationIntent.putExtra("sender", C2DM_SENDER); + context.startService(registrationIntent); + } + + /** unregister with c2dm service */ + public static void unregister() { + Preferences.setString(PREF_REGISTRATION, null); + Context context = ContextManager.getContext(); + Intent unregIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER"); + unregIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0)); + context.startService(unregIntent); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java new file mode 100644 index 000000000..ccfbe7f66 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java @@ -0,0 +1,50 @@ +package com.todoroo.astrid.actfm; + +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.actfm.sync.ActFmSyncProvider; +import com.todoroo.astrid.service.StatisticsService; +import com.todoroo.astrid.sync.SyncBackgroundService; +import com.todoroo.astrid.sync.SyncProvider; +import com.todoroo.astrid.sync.SyncProviderUtilities; + +/** + * SynchronizationService is the service that performs Astrid's background + * synchronization with online task managers. Starting this service + * schedules a repeating alarm which handles the synchronization + * + * @author Tim Su + * + */ +public class ActFmBackgroundService extends SyncBackgroundService { + + @Autowired ActFmPreferenceService actFmPreferenceService; + + public ActFmBackgroundService() { + DependencyInjectionService.getInstance().inject(this); + } + + @Override + protected SyncProvider getSyncProvider() { + return new ActFmSyncProvider(); + } + + @Override + protected SyncProviderUtilities getSyncUtilities() { + return actFmPreferenceService; + } + + @Override + public void onCreate() { + super.onCreate(); + StatisticsService.sessionStart(this); + } + + @Override + public void onDestroy() { + StatisticsService.sessionStop(this); + super.onDestroy(); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java new file mode 100644 index 000000000..0d2354f28 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java @@ -0,0 +1,249 @@ +/* + * ASTRID: Android's Simple Task Recording Dashboard + * + * Copyright (c) 2009 Tim Su + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.todoroo.astrid.actfm; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; + +import org.json.JSONObject; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import com.facebook.android.AsyncFacebookRunner; +import com.facebook.android.AsyncFacebookRunner.RequestListener; +import com.facebook.android.AuthListener; +import com.facebook.android.Facebook; +import com.facebook.android.FacebookError; +import com.facebook.android.LoginButton; +import com.facebook.android.Util; +import com.timsu.astrid.C2DMReceiver; +import com.timsu.astrid.R; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.utility.DialogUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.actfm.sync.ActFmInvoker; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.service.AstridDependencyInjector; +import com.todoroo.astrid.service.TaskService; + +/** + * This activity allows users to sign in or log in to Producteev + * + * @author arne.jans + * + */ +public class ActFmLoginActivity extends Activity implements AuthListener { + + public static final String APP_ID = "183862944961271"; //$NON-NLS-1$ + + @Autowired TaskService taskService; + @Autowired ActFmPreferenceService actFmPreferenceService; + private final ActFmInvoker actFmInvoker = new ActFmInvoker(); + + private Facebook facebook; + private AsyncFacebookRunner facebookRunner; + private TextView errors; + private boolean noSync = false; + + // --- ui initialization + + static { + AstridDependencyInjector.initialize(); + } + + public static final String EXTRA_DO_NOT_SYNC = "nosync"; //$NON-NLS-1$ + + public ActFmLoginActivity() { + super(); + DependencyInjectionService.getInstance().inject(this); + } + + @SuppressWarnings("nls") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ContextManager.setContext(this); + + setContentView(R.layout.sharing_login_activity); + setTitle(R.string.sharing_SLA_title); + + noSync = getIntent().getBooleanExtra(EXTRA_DO_NOT_SYNC, false); + + facebook = new Facebook(APP_ID); + facebookRunner = new AsyncFacebookRunner(facebook); + + errors = (TextView) findViewById(R.id.error); + LoginButton loginButton = (LoginButton) findViewById(R.id.fb_login); + loginButton.init(this, facebook, this, new String[] { + "email", + "offline_access", + "publish_stream" + }); + + getWindow().setFormat(PixelFormat.RGBA_8888); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DITHER); + + setResult(RESULT_CANCELED); + } + + // --- facebook handler + + @SuppressWarnings("nls") + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + String error = data.getStringExtra("error"); + if (error == null) { + error = data.getStringExtra("error_type"); + } + String token = data.getStringExtra("access_token"); + if(error != null) { + onFBAuthFail(error); + } else if(token == null) { + onFBAuthFail("Something went wrong! Please try again."); + } else { + facebook.setAccessToken(token); + onFBAuthSucceed(); + } + errors.setVisibility(View.GONE); + } + + public void onFBAuthSucceed() { + createUserAccountFB(); + } + + public void onFBAuthFail(String error) { + DialogUtilities.okDialog(this, getString(R.string.sharing_SLA_title), + android.R.drawable.ic_dialog_alert, error, null); + } + + @Override + public void onFBAuthCancel() { + // do nothing + } + + // --- astrid social handler + + ProgressDialog progressDialog; + + /** + * Create user account via FB + */ + public void createUserAccountFB() { + progressDialog = DialogUtilities.progressDialog(this, + getString(R.string.DLG_please_wait)); + facebookRunner.request("me", new SLARequestListener()); //$NON-NLS-1$ + } + + private class SLARequestListener implements RequestListener { + + @SuppressWarnings("nls") + @Override + public void onComplete(String response, Object state) { + JSONObject json; + try { + json = Util.parseJson(response); + String name = json.getString("name"); //$NON-NLS-1$ + String email = json.getString("email"); //$NON-NLS-1$ + + JSONObject result = actFmInvoker.authenticate(email, name, ActFmInvoker.PROVIDER_FACEBOOK, + facebook.getAccessToken()); + + String token = actFmInvoker.getToken(); + actFmPreferenceService.setToken(token); + + if(Preferences.getStringValue(R.string.actfm_APr_interval_key) == null) + Preferences.setStringFromInteger(R.string.actfm_APr_interval_key, 3600); + + Preferences.setLong(ActFmPreferenceService.PREF_USER_ID, + result.optLong("id")); + Preferences.setString(ActFmPreferenceService.PREF_NAME, result.optString("name")); + Preferences.setString(ActFmPreferenceService.PREF_EMAIL, result.optString("email")); + Preferences.setString(ActFmPreferenceService.PREF_PICTURE, result.optString("picture")); + + C2DMReceiver.register(); + + progressDialog.dismiss(); + setResult(RESULT_OK); + finish(); + + if(!noSync) { + synchronize(); + } + + } catch (Throwable e) { + handleError(e); + } + } + + private void handleError(final Throwable e) { + progressDialog.dismiss(); + Log.e("astrid-sharing", "error-doing-sla", e); //$NON-NLS-1$ //$NON-NLS-2$ + + runOnUiThread(new Runnable() { + @Override + public void run() { + errors.setText(e.toString()); + errors.setVisibility(View.VISIBLE); + } + }); + } + + @Override + public void onFacebookError(FacebookError e, Object state) { + handleError(e); + } + + @Override + public void onFileNotFoundException(FileNotFoundException e, + Object state) { + handleError(e); + } + + @Override + public void onIOException(IOException e, Object state) { + handleError(e); + } + + @Override + public void onMalformedURLException(MalformedURLException e, + Object state) { + handleError(e); + } + + } + + public void synchronize() { + startService(new Intent(null, null, + this, ActFmBackgroundService.class)); + } + +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java new file mode 100644 index 000000000..4cc89bcc9 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java @@ -0,0 +1,48 @@ +package com.todoroo.astrid.actfm; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.actfm.sync.ActFmSyncProvider; +import com.todoroo.astrid.sync.SyncProviderPreferences; +import com.todoroo.astrid.sync.SyncProviderUtilities; + +/** + * Displays synchronization preferences and an action panel so users can + * initiate actions from the menu. + * + * @author timsu + * + */ +public class ActFmPreferences extends SyncProviderPreferences { + + @Autowired ActFmPreferenceService actFmPreferenceService; + + @Override + public int getPreferenceResource() { + return R.xml.preferences_actfm; + } + + @Override + public void startSync() { + new ActFmSyncProvider().synchronize(this); + finish(); + } + + @Override + public void logOut() { + new ActFmSyncProvider().signOut(); + } + + @Override + public SyncProviderUtilities getUtilities() { + return actFmPreferenceService; + } + + @Override + protected void onPause() { + super.onPause(); + new ActFmBackgroundService().scheduleService(); + } + +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmSyncActionExposer.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmSyncActionExposer.java new file mode 100644 index 000000000..2bfd7a4e9 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmSyncActionExposer.java @@ -0,0 +1,48 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.actfm; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.SyncAction; + +/** + * Exposes sync action + * + */ +public class ActFmSyncActionExposer extends BroadcastReceiver { + + @Autowired ActFmPreferenceService actFmPreferenceService; + + @Override + public void onReceive(Context context, Intent intent) { + ContextManager.setContext(context); + DependencyInjectionService.getInstance().inject(this); + + // if we aren't logged in, don't expose sync action + if(!actFmPreferenceService.isLoggedIn()) + return; + + Intent syncIntent = new Intent(null, null, + context, ActFmBackgroundService.class); + PendingIntent pendingIntent = PendingIntent.getService(context, 0, syncIntent, PendingIntent.FLAG_UPDATE_CURRENT); + SyncAction syncAction = new SyncAction(context.getString(R.string.actfm_APr_header), + pendingIntent); + + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ActFmPreferenceService.IDENTIFIER); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, syncAction); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleActivity.java new file mode 100644 index 000000000..9c972785e --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleActivity.java @@ -0,0 +1,575 @@ +package com.todoroo.astrid.actfm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.timsu.astrid.R; +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.utility.DialogUtilities; +import com.todoroo.astrid.actfm.sync.ActFmInvoker; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.actfm.sync.ActFmSyncService; +import com.todoroo.astrid.actfm.sync.ActFmSyncService.JsonHelper; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.service.MetadataService; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.service.TaskService; +import com.todoroo.astrid.service.ThemeService; +import com.todoroo.astrid.tags.TagService; +import com.todoroo.astrid.ui.PeopleContainer; +import com.todoroo.astrid.ui.PeopleContainer.OnAddNewPersonListener; +import com.todoroo.astrid.utility.Flags; + +public class EditPeopleActivity extends Activity { + + public static final String EXTRA_TASK_ID = "task"; //$NON-NLS-1$ + + private static final int REQUEST_LOG_IN = 0; + + private Task task; + + private final ArrayList nonSharedTags = new ArrayList(); + + @Autowired ActFmPreferenceService actFmPreferenceService; + + @Autowired ActFmSyncService actFmSyncService; + + @Autowired TaskService taskService; + + @Autowired MetadataService metadataService; + + @Autowired ExceptionService exceptionService; + + @Autowired TagDataService tagDataService; + + private PeopleContainer sharedWithContainer; + + private CheckBox cbFacebook; + + private CheckBox cbTwitter; + + private Spinner assignedSpinner; + + private EditText assignedCustom; + + private final ArrayList spinnerValues = new ArrayList(); + + public EditPeopleActivity() { + DependencyInjectionService.getInstance().inject(this); + } + + // --- UI initialization + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.edit_people_activity); + setTitle(getString(R.string.actfm_EPA_title)); + ThemeService.applyTheme(this); + + task = taskService.fetchById( + getIntent().getLongExtra(EXTRA_TASK_ID, 41L), Task.ID, Task.REMOTE_ID, + Task.TITLE, Task.USER, Task.USER_ID, Task.SHARED_WITH, Task.FLAGS); + if(task == null) { + finish(); + return; + } + + ((TextView) findViewById(R.id.title)).setText(task.getValue(Task.TITLE)); + sharedWithContainer = (PeopleContainer) findViewById(R.id.share_container); + assignedCustom = (EditText) findViewById(R.id.assigned_custom); + assignedSpinner = (Spinner) findViewById(R.id.assigned_spinner); + cbFacebook = (CheckBox) findViewById(R.id.checkbox_facebook); + cbTwitter = (CheckBox) findViewById(R.id.checkbox_twitter); + + sharedWithContainer.addPerson(""); //$NON-NLS-1$ + setUpListeners(); + setUpData(); + } + + @SuppressWarnings("nls") + private void setUpData() { + try { + JSONObject sharedWith; + if(task.getValue(Task.SHARED_WITH).length() > 0) + sharedWith = new JSONObject(task.getValue(Task.SHARED_WITH)); + else + sharedWith = new JSONObject(); + + cbFacebook.setChecked(sharedWith.optBoolean("fb", false)); + cbTwitter.setChecked(sharedWith.optBoolean("tw", false)); + + final ArrayList sharedPeople = new ArrayList(); + JSONArray people = sharedWith.optJSONArray("p"); + if(people != null) { + for(int i = 0; i < people.length(); i++) { + String person = people.getString(i); + TextView textView = sharedWithContainer.addPerson(person); + textView.setEnabled(false); + sharedPeople.add(PeopleContainer.createUserJson(textView)); + } + } + + new Thread(new Runnable() { + @Override + public void run() { + TodorooCursor tags = TagService.getInstance().getTags(task.getId()); + try { + Metadata metadata = new Metadata(); + for(tags.moveToFirst(); !tags.isAfterLast(); tags.moveToNext()) { + metadata.readFromCursor(tags); + final String tag = metadata.getValue(TagService.TAG); + TagData tagData = tagDataService.getTag(tag, TagData.MEMBER_COUNT, TagData.MEMBERS, TagData.USER); + if(tagData != null && tagData.getValue(TagData.MEMBER_COUNT) > 0) { + runOnUiThread(new Runnable() { + @Override + public void run() { + TextView textView = sharedWithContainer.addPerson("#" + tag); + textView.setEnabled(false); + } + }); + JSONArray members = new JSONArray(tagData.getValue(TagData.MEMBERS)); + for(int i = 0; i < members.length(); i++) + sharedPeople.add(members.getJSONObject(i)); + if(!TextUtils.isEmpty(tagData.getValue(TagData.USER))) + sharedPeople.add(new JSONObject(tagData.getValue(TagData.USER))); + } else { + nonSharedTags.add((Metadata) metadata.clone()); + } + } + + buildAssignedToSpinner(sharedPeople); + } catch (JSONException e) { + exceptionService.reportError("json-reading-data", e); + } finally { + tags.close(); + } + } + }).start(); + + } catch (JSONException e) { + exceptionService.reportError("json-reading-data", e); + } + } + + private class AssignedToUser { + public String label; + public JSONObject user; + + public AssignedToUser(String label, JSONObject user) { + super(); + this.label = label; + this.user = user; + } + + @Override + public String toString() { + return label; + } + } + + @SuppressWarnings("nls") + private void buildAssignedToSpinner(ArrayList sharedPeople) throws JSONException { + HashSet userIds = new HashSet(); + HashSet emails = new HashSet(); + HashMap names = new HashMap(); + + JSONObject myself = new JSONObject(); + myself.put("id", 0L); + sharedPeople.add(0, myself); + if(task.getValue(Task.USER_ID) != 0) { + JSONObject user = new JSONObject(task.getValue(Task.USER)); + sharedPeople.add(0, user); + } + + // de-duplicate by user id and/or email + spinnerValues.clear(); + for(int i = 0; i < sharedPeople.size(); i++) { + JSONObject person = sharedPeople.get(i); + if(person == null) + continue; + long id = person.optLong("id", -1); + if(id == ActFmPreferenceService.userId() || (id > -1 && userIds.contains(id))) + continue; + userIds.add(id); + + String email = person.optString("email"); + if(!TextUtils.isEmpty(email) && emails.contains(email)) + continue; + emails.add(email); + + String name = person.optString("name"); + if(id == 0) + name = getString(R.string.actfm_EPA_assign_me); + AssignedToUser atu = new AssignedToUser(name, person); + spinnerValues.add(atu); + if(names.containsKey(name)) { + AssignedToUser user = names.get(name); + if(user != null && user.user.has("email")) { + user.label += " (" + user.user.optString("email") + ")"; + names.put(name, null); + } + if(!TextUtils.isEmpty("email")) + atu.label += " (" + email + ")"; + } else if(TextUtils.isEmpty(name)) { + if(!TextUtils.isEmpty("email")) + atu.label = email; + else + spinnerValues.remove(atu); + } else + names.put(name, atu); + } + + spinnerValues.add(new AssignedToUser(getString(R.string.actfm_EPA_assign_custom), null)); + + final ArrayAdapter usersAdapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, spinnerValues); + usersAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + runOnUiThread(new Runnable() { + @Override + public void run() { + assignedSpinner.setAdapter(usersAdapter); + } + }); + } + + private void setUpListeners() { + final View assignedClear = findViewById(R.id.assigned_clear); + + assignedSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View v, + int index, long id) { + if(index == spinnerValues.size() - 1) { + assignedCustom.setVisibility(View.VISIBLE); + assignedClear.setVisibility(View.VISIBLE); + assignedSpinner.setVisibility(View.GONE); + } + } + @Override + public void onNothingSelected(AdapterView arg0) { + // + } + }); + findViewById(R.id.discard).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + findViewById(R.id.discard).requestFocus(); + + findViewById(R.id.save).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + save(); + } + }); + + assignedClear.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + assignedCustom.setVisibility(View.GONE); + assignedClear.setVisibility(View.GONE); + assignedCustom.setText(""); //$NON-NLS-1$ + assignedSpinner.setVisibility(View.VISIBLE); + assignedSpinner.setSelection(0); + } + }); + + sharedWithContainer.setOnAddNewPerson(new OnAddNewPersonListener() { + @Override + public void textChanged(String text) { + findViewById(R.id.share_additional).setVisibility(View.VISIBLE); + if(text.indexOf('@') > -1) { + findViewById(R.id.tag_label).setVisibility(View.VISIBLE); + findViewById(R.id.tag_name).setVisibility(View.VISIBLE); + } + } + }); + } + + // --- events + + /** Save sharing settings */ + @SuppressWarnings("nls") + private void save() { + if(!actFmPreferenceService.isLoggedIn()) { + startActivityForResult(new Intent(this, ActFmLoginActivity.class), + REQUEST_LOG_IN); + return; + } + + setResult(RESULT_OK); + Flags.set(Flags.REFRESH); + try { + JSONObject userJson; + AssignedToUser assignedTo = (AssignedToUser) assignedSpinner.getSelectedItem(); + if(assignedTo == null) + userJson = PeopleContainer.createUserJson(assignedCustom); + else + userJson = assignedTo.user; + if(userJson == null || userJson.optLong("id", -1) == 0) { + task.setValue(Task.USER_ID, 0L); + task.setValue(Task.USER, "{}"); + } else { + task.setValue(Task.USER_ID, userJson.optLong("id", -1)); + task.setValue(Task.USER, userJson.toString()); + } + + ArrayList metadata = new ArrayList(nonSharedTags); + JSONObject sharedWith = parseSharedWithAndTags(metadata); + task.setValue(Task.SHARED_WITH, sharedWith.toString()); + + metadataService.synchronizeMetadata(task.getId(), metadata, MetadataCriteria.withKey(TagService.KEY)); + + Flags.set(Flags.SUPPRESS_SYNC); + taskService.save(task); + shareTask(sharedWith, metadata); + } catch (JSONException e) { + exceptionService.displayAndReportError(this, "save-people", e); + } catch (ParseSharedException e) { + e.view.setTextColor(Color.RED); + e.view.requestFocus(); + System.err.println(e.message); + DialogUtilities.okDialog(this, e.message, null); + } + } + + private class ParseSharedException extends Exception { + private static final long serialVersionUID = -4135848250086302970L; + public TextView view; + public String message; + + public ParseSharedException(TextView view, String message) { + this.view = view; + this.message = message; + } + } + + @SuppressWarnings("nls") + private JSONObject parseSharedWithAndTags(ArrayList metadata) throws + JSONException, ParseSharedException { + JSONObject sharedWith = new JSONObject(); + if(cbFacebook.isChecked()) + sharedWith.put("fb", true); + if(cbTwitter.isChecked()) + sharedWith.put("tw", true); + + JSONArray peopleList = new JSONArray(); + for(int i = 0; i < sharedWithContainer.getChildCount(); i++) { + TextView textView = sharedWithContainer.getTextView(i); + textView.setTextAppearance(this, android.R.style.TextAppearance_Medium_Inverse); + String text = textView.getText().toString(); + + if(text.length() == 0) + continue; + if(text.startsWith("#")) { + text = text.substring(1); + TagData tagData = tagDataService.getTag(text, TagData.REMOTE_ID); + if(tagData == null) + throw new ParseSharedException(textView, + getString(R.string.actfm_EPA_invalid_tag, text)); + Metadata tag = new Metadata(); + tag.setValue(Metadata.KEY, TagService.KEY); + tag.setValue(TagService.TAG, text); + tag.setValue(TagService.REMOTE_ID, tagData.getValue(TagData.REMOTE_ID)); + metadata.add(tag); + + } else { + if(text.indexOf('@') == -1) + throw new ParseSharedException(textView, + getString(R.string.actfm_EPA_invalid_email, text)); + peopleList.put(text); + } + } + sharedWith.put("p", peopleList); + + return sharedWith; + } + + @SuppressWarnings("nls") + private void shareTask(final JSONObject sharedWith, final ArrayList metadata) { + final JSONArray emails = sharedWith.optJSONArray("p"); + + final ProgressDialog pd = DialogUtilities.progressDialog(this, + getString(R.string.DLG_please_wait)); + new Thread() { + @Override + public void run() { + ActFmInvoker invoker = new ActFmInvoker(actFmPreferenceService.getToken()); + try { + if(task.getValue(Task.REMOTE_ID) == 0) { + actFmSyncService.pushTask(task.getId()); + task.setValue(Task.REMOTE_ID, taskService.fetchById(task.getId(), + Task.REMOTE_ID).getValue(Task.REMOTE_ID)); + } + + Object[] args = buildSharingArgs(emails, metadata); + JSONObject result = invoker.invoke("task_share", args); + + sharedWith.remove("p"); + task.setValue(Task.SHARED_WITH, sharedWith.toString()); + task.setValue(Task.DETAILS_DATE, 0L); + + readTagData(result.getJSONArray("tags")); + JsonHelper.readUser(result.getJSONObject("assignee"), + task, Task.USER_ID, Task.USER); + Flags.set(Flags.SUPPRESS_SYNC); + taskService.save(task); + + int count = result.optInt("shared", 0); + final String toast; + if(count > 0) + toast = getString(R.string.actfm_EPA_emailed_toast, + getResources().getQuantityString(R.plurals.Npeople, count, count)); + else + toast = getString(R.string.actfm_EPA_saved_toast); + + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH); + ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + + runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(EditPeopleActivity.this, toast, Toast.LENGTH_LONG).show(); + finish(); + } + }); + } catch (IOException e) { + DialogUtilities.okDialog(EditPeopleActivity.this, getString(R.string.SyP_ioerror), + android.R.drawable.ic_dialog_alert, e.toString(), null); + } catch (JSONException e) { + DialogUtilities.okDialog(EditPeopleActivity.this, getString(R.string.SyP_ioerror), + android.R.drawable.ic_dialog_alert, e.toString(), null); + } finally { + runOnUiThread(new Runnable() { + public void run() { + pd.dismiss(); + } + }); + } + } + + }.start(); + } + + @SuppressWarnings("nls") + private void readTagData(JSONArray tags) throws JSONException { + ArrayList metadata = new ArrayList(); + for(int i = 0; i < tags.length(); i++) { + JSONObject tagObject = tags.getJSONObject(i); + TagData tagData = tagDataService.getTag(tagObject.getString("name"), TagData.ID); + if(tagData == null) + tagData = new TagData(); + ActFmSyncService.JsonHelper.tagFromJson(tagObject, tagData); + tagDataService.save(tagData); + + Metadata tagMeta = new Metadata(); + tagMeta.setValue(Metadata.KEY, TagService.KEY); + tagMeta.setValue(TagService.TAG, tagData.getValue(TagData.NAME)); + tagMeta.setValue(TagService.REMOTE_ID, tagData.getValue(TagData.REMOTE_ID)); + metadata.add(tagMeta); + } + + metadataService.synchronizeMetadata(task.getId(), metadata, MetadataCriteria.withKey(TagService.KEY)); + } + + @SuppressWarnings("nls") + protected Object[] buildSharingArgs(JSONArray emails, ArrayList + tags) throws JSONException { + ArrayList values = new ArrayList(); + long currentTaskID = task.getValue(Task.REMOTE_ID); + values.add("id"); + values.add(currentTaskID); + + for(int i = 0; i < emails.length(); i++) { + String email = emails.optString(i); + if(email == null || email.indexOf('@') == -1) + continue; + values.add("emails[]"); + values.add(email); + } + + for(int i = 0; i < tags.size(); i++) { + Metadata tag = tags.get(i); + if(tag.containsNonNullValue(TagService.REMOTE_ID) && + tag.getValue(TagService.REMOTE_ID) > 0) { + values.add("tag_ids[]"); + values.add(tag.getValue(TagService.REMOTE_ID)); + } else { + values.add("tags[]"); + values.add(tag.getValue(TagService.TAG)); + } + } + + values.add("assignee"); + if(task.getValue(Task.USER_ID) == 0) { + values.add(""); + } else { + if(task.getValue(Task.USER_ID) > 0) + values.add(task.getValue(Task.USER_ID)); + else { + JSONObject user = new JSONObject(task.getValue(Task.USER)); + values.add(user.getString("email")); + } + } + + String message = ((TextView) findViewById(R.id.message)).getText().toString(); + if(!TextUtils.isEmpty(message) && findViewById(R.id.share_additional).getVisibility() == View.VISIBLE) { + values.add("message"); + values.add(message); + } + + String tag = ((TextView) findViewById(R.id.tag_name)).getText().toString(); + if(!TextUtils.isEmpty(tag)) { + values.add("tag"); + values.add(tag); + } + + return values.toArray(new Object[values.size()]); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if(requestCode == REQUEST_LOG_IN) { + if(resultCode == RESULT_OK) + save(); + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleExposer.java b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleExposer.java new file mode 100644 index 000000000..06d8165f3 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleExposer.java @@ -0,0 +1,61 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.actfm; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.TaskAction; +import com.todoroo.astrid.api.TaskDecoration; + +/** + * Exposes {@link TaskDecoration} for timers + * + * @author Tim Su + * + */ +public class EditPeopleExposer extends BroadcastReceiver { + + private static final String ACTION = "com.todoroo.astrid.EDIT_PEOPLE"; //$NON-NLS-1$ + + @Override + public void onReceive(Context context, Intent intent) { + ContextManager.setContext(context); + long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + if(AstridApiConstants.BROADCAST_REQUEST_ACTIONS.equals(intent.getAction())) { + final String label = context.getString(R.string.EPE_action); + final Drawable drawable = context.getResources().getDrawable(R.drawable.tango_users); + Intent newIntent = new Intent(ACTION); + newIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + Bitmap icon = ((BitmapDrawable)drawable).getBitmap(); + TaskAction action = new TaskAction(label, + PendingIntent.getBroadcast(context, (int)taskId, newIntent, 0), icon); + + // transmit + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_ACTIONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ActFmPreferenceService.IDENTIFIER); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, action); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } else if(ACTION.equals(intent.getAction())) { + Intent launchIntent = new Intent(context, EditPeopleActivity.class); + launchIntent.putExtra(EditPeopleActivity.EXTRA_TASK_ID, taskId); + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ContextManager.getContext().startActivity(launchIntent); + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/sharing/SharingDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ProjectDetailExposer.java similarity index 61% rename from astrid/plugin-src/com/todoroo/astrid/sharing/SharingDetailExposer.java rename to astrid/plugin-src/com/todoroo/astrid/actfm/ProjectDetailExposer.java index 2b95624a4..912c1dfa7 100644 --- a/astrid/plugin-src/com/todoroo/astrid/sharing/SharingDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ProjectDetailExposer.java @@ -1,16 +1,15 @@ /** * See the file "LICENSE" for the full license governing this code. */ -package com.todoroo.astrid.sharing; +package com.todoroo.astrid.actfm; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.api.AstridApiConstants; -import com.todoroo.astrid.core.PluginServices; -import com.todoroo.astrid.data.Metadata; /** * Exposes Task Detail for notes @@ -18,7 +17,7 @@ import com.todoroo.astrid.data.Metadata; * @author Tim Su * */ -public class SharingDetailExposer extends BroadcastReceiver { +public class TagDataDetailExposer extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -33,19 +32,21 @@ public class SharingDetailExposer extends BroadcastReceiver { // transmit Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, + ActFmPreferenceService.IDENTIFIER); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); } public String getTaskDetails(long id) { - Metadata metadata = PluginServices.getMetadataByTaskAndWithKey(id, SharingFields.METADATA_KEY); - if(metadata == null) + /*Task task = PluginServices.getTaskService().fetchById(id, Task.PROJECT_ID); + if(task == null) + return null; + TagData tagData = PluginServices.getTagDataService().fetchById(task.getValue(Task.PROJECT_ID), TagData.TITLE); + if(tagData == null)*/ return null; - if(metadata.getValue(SharingFields.PRIVACY) == SharingFields.PRIVACY_PUBLIC) - return " Public"; //$NON-NLS-1$ - - return null; + // return " " + tagData.getValue(TagData.TITLE); //$NON-NLS-1$ } } diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ProjectListActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ProjectListActivity.java new file mode 100644 index 000000000..aa841ef9f --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ProjectListActivity.java @@ -0,0 +1,229 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.actfm; + +import java.util.concurrent.atomic.AtomicReference; + +import android.app.ListActivity; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageButton; +import android.widget.ImageView; + +import com.timsu.astrid.R; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.andlib.sql.QueryTemplate; +import com.todoroo.andlib.utility.DialogUtilities; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.actfm.sync.ActFmSyncService; +import com.todoroo.astrid.activity.TaskListActivity; +import com.todoroo.astrid.adapter.TagDataAdapter; +import com.todoroo.astrid.dao.TagDataDao.TagDataCriteria; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.service.AstridDependencyInjector; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.service.StatisticsService; +import com.todoroo.astrid.service.ThemeService; + +/** + * Activity that displays a user's task lists and allows users + * to filter their task list. + * + * @author Tim Su + * + */ +public class TagDataListActivity extends ListActivity implements OnItemClickListener { + + // --- constants + + private static final int REQUEST_LOG_IN = 1; + private static final int REQUEST_SHOW_GOAL = 2; + + private static final int MENU_REFRESH_ID = Menu.FIRST + 0; + + // --- instance variables + + @Autowired ExceptionService exceptionService; + @Autowired TagDataService tagDataService; + @Autowired ActFmPreferenceService actFmPreferenceService; + @Autowired ActFmSyncService actFmSyncService; + + protected TagDataAdapter adapter = null; + protected AtomicReference queryTemplate = new AtomicReference(); + + /* ====================================================================== + * ======================================================= initialization + * ====================================================================== */ + + static { + AstridDependencyInjector.initialize(); + } + + /** Called when loading up the activity */ + @Override + protected void onCreate(Bundle savedInstanceState) { + DependencyInjectionService.getInstance().inject(this); + requestWindowFeature(Window.FEATURE_NO_TITLE); + super.onCreate(savedInstanceState); + + setContentView(R.layout.tagData_list_activity); + ThemeService.applyTheme(this); + + if(!actFmPreferenceService.isLoggedIn()) { + Intent login = new Intent(this, ActFmLoginActivity.class); + login.putExtra(ActFmLoginActivity.EXTRA_DO_NOT_SYNC, true); + startActivityForResult(login, REQUEST_LOG_IN); + } + + initializeUIComponents(); + setUpList(); + refreshList(false); + } + + @SuppressWarnings("nls") + private void initializeUIComponents() { + ((ImageButton) findViewById(R.id.extendedAddButton)).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + DialogUtilities.okDialog(TagDataListActivity.this, "unsupported", null); + } + }); + + ((ImageButton) findViewById(R.id.extendedAddButton)).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + DialogUtilities.okDialog(TagDataListActivity.this, "unsupported", null); + } + }); + + ((ImageView) findViewById(R.id.goals)).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(TagDataListActivity.this, TaskListActivity.class); + startActivity(intent); + finish(); + } + }); + + + } + + /** + * Create options menu (displayed when user presses menu key) + * + * @return true if menu should be displayed + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if(menu.size() > 0) + return true; + + MenuItem item; + + item = menu.add(Menu.NONE, MENU_REFRESH_ID, Menu.NONE, + R.string.PLA_menu_refresh); + item.setIcon(R.drawable.ic_menu_refresh); + + return true; + } + + /* ====================================================================== + * ============================================================ lifecycle + * ====================================================================== */ + + @Override + protected void onStart() { + super.onStart(); + StatisticsService.sessionStart(this); + StatisticsService.reportEvent("goal-list"); //$NON-NLS-1$ + } + + @Override + protected void onStop() { + super.onStop(); + StatisticsService.sessionStop(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if(requestCode == REQUEST_LOG_IN) { + if(resultCode == RESULT_CANCELED) + finish(); + else + refreshList(true); + } else + super.onActivityResult(requestCode, resultCode, data); + } + + /* ====================================================================== + * ====================================================== populating list + * ====================================================================== */ + + /** Sets up the coach list adapter */ + protected void setUpList() { + queryTemplate.set(new QueryTemplate().where(TagDataCriteria.isTeam()).toString()); + TodorooCursor currentCursor = tagDataService.fetchFiltered(queryTemplate.get(), + null, TagData.PROPERTIES); + startManagingCursor(currentCursor); + + adapter = new TagDataAdapter(this, R.layout.tagData_adapter_row, + currentCursor, queryTemplate, false, null); + setListAdapter(adapter); + + getListView().setOnItemClickListener(this); + } + + /** refresh the list with latest data from the web */ + private void refreshList(boolean manual) { + actFmSyncService.fetchTagDataDashboard(manual, new Runnable() { + @Override + public void run() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Cursor cursor = adapter.getCursor(); + cursor.requery(); + startManagingCursor(cursor); + } + }); + } + }); + } + + /* ====================================================================== + * ============================================================== actions + * ====================================================================== */ + + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + Intent intent = new Intent(this, TagDataViewActivity.class); + intent.putExtra(TagDataViewActivity.EXTRA_PROJECT_ID, id); + startActivityForResult(intent, REQUEST_SHOW_GOAL); + } + + @Override + public boolean onMenuItemSelected(int featureId, final MenuItem item) { + + // handle my own menus + switch (item.getItemId()) { + case MENU_REFRESH_ID: { + refreshList(true); + return true; + } + } + + return false; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ShowProjectExposer.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ShowProjectExposer.java new file mode 100644 index 000000000..c683d863f --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ShowProjectExposer.java @@ -0,0 +1,57 @@ +package com.todoroo.astrid.actfm; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.TaskAction; +import com.todoroo.astrid.core.PluginServices; +import com.todoroo.astrid.data.TagData; + +public class ShowTagDataExposer extends BroadcastReceiver { + + private static final String FILTER_ACTION = "com.todoroo.astrid.SHOW_PROJECT"; //$NON-NLS-1$ + + @Override + public void onReceive(Context context, Intent intent) { + ContextManager.setContext(context); + long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + TagData tagData = PluginServices.getTagDataService().getTagData(taskId, + TagData.ID, TagData.TITLE); + if(tagData == null) + return; + + if(AstridApiConstants.BROADCAST_REQUEST_ACTIONS.equals(intent.getAction())) { + final String label = tagData.getValue(TagData.TITLE); + final Drawable drawable = context.getResources().getDrawable(R.drawable.tango_users); + Intent newIntent = new Intent(FILTER_ACTION); + newIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + Bitmap icon = ((BitmapDrawable)drawable).getBitmap(); + TaskAction action = new TaskAction(label, + PendingIntent.getBroadcast(context, (int)taskId, newIntent, 0), icon); + + // transmit + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_ACTIONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ActFmPreferenceService.IDENTIFIER); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, action); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } else if(FILTER_ACTION.equals(intent.getAction())) { + Intent launchIntent = new Intent(context, TagDataViewActivity.class); + launchIntent.putExtra(TagDataViewActivity.EXTRA_PROJECT_ID, tagData.getId()); + ContextManager.getContext().startActivity(launchIntent); + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java new file mode 100644 index 000000000..d54da824d --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java @@ -0,0 +1,633 @@ +package com.todoroo.astrid.actfm; + +import greendroid.widget.AsyncImageView; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.TabHost; +import android.widget.TabHost.OnTabChangeListener; +import android.widget.TabWidget; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import com.timsu.astrid.R; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.service.NotificationManager; +import com.todoroo.andlib.service.NotificationManager.AndroidNotificationManager; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Query; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.DialogUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; +import com.todoroo.astrid.actfm.sync.ActFmSyncService; +import com.todoroo.astrid.activity.TaskListActivity; +import com.todoroo.astrid.adapter.UpdateAdapter; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.core.SortHelper; +import com.todoroo.astrid.dao.UpdateDao; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.data.Update; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.tags.TagService; +import com.todoroo.astrid.ui.PeopleContainer; +import com.todoroo.astrid.utility.Flags; + +public class TagViewActivity extends TaskListActivity implements OnTabChangeListener { + + private static final String LAST_FETCH_KEY = "tag-fetch-"; //$NON-NLS-1$ + + public static final String BROADCAST_TAG_ACTIVITY = AstridApiConstants.PACKAGE + ".TAG_ACTIVITY"; //$NON-NLS-1$ + + public static final String EXTRA_TAG_NAME = "tag"; //$NON-NLS-1$ + public static final String EXTRA_TAG_REMOTE_ID = "remoteId"; //$NON-NLS-1$ + public static final String EXTRA_START_TAB = "tab"; //$NON-NLS-1$ + + protected static final int MENU_REFRESH_ID = MENU_SYNC_ID; + + protected static final int REQUEST_CODE_CAMERA = 1; + protected static final int REQUEST_CODE_PICTURE = 2; + + + private TagData tagData; + + @Autowired TagDataService tagDataService; + + @Autowired ActFmSyncService actFmSyncService; + + @Autowired UpdateDao updateDao; + + private TabHost tabHost; + private UpdateAdapter updateAdapter; + private PeopleContainer tagMembers; + private EditText addCommentField; + private AsyncImageView picture; + private EditText tagName; + private boolean dataLoaded = false; + + // --- UI initialization + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getListView().setOnKeyListener(null); + + // allow for text field entry, needed for android bug #2516 + OnTouchListener onTouch = new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + v.requestFocusFromTouch(); + return false; + } + }; + ((EditText) findViewById(R.id.quickAddText)).setOnTouchListener(onTouch); + ((EditText) findViewById(R.id.commentField)).setOnTouchListener(onTouch); + + if(getIntent().hasExtra(EXTRA_START_TAB)) + tabHost.setCurrentTab(getIntent().getIntExtra(EXTRA_START_TAB, 0)); + } + + @SuppressWarnings("nls") + @Override + protected View getListBody(ViewGroup root) { + ViewGroup parent = (ViewGroup) getLayoutInflater().inflate(R.layout.task_list_body_tag, root, false); + ViewGroup tabContent = (ViewGroup) parent.findViewById(android.R.id.tabcontent); + + String[] tabLabels = getResources().getStringArray(R.array.TVA_tabs); + tabHost = (TabHost) parent.findViewById(android.R.id.tabhost); + TabWidget tabWidget = (TabWidget) parent.findViewById(android.R.id.tabs); + tabHost.setup(); + + // set up tabs + View taskList = super.getListBody(parent); + tabContent.addView(taskList); + addTab(tabWidget, taskList.getId(), "tasks", tabLabels[0]); + addTab(tabWidget, R.id.tab_updates, "updates", tabLabels[1]); + addTab(tabWidget, R.id.tab_settings, "members", tabLabels[2]); + + tabHost.setOnTabChangedListener(this); + + return parent; + } + + private void addTab(TabWidget tabWidget, int contentId, String id, String label) { + TabHost.TabSpec spec = tabHost.newTabSpec(id); + spec.setContent(contentId); + TextView textIndicator = (TextView) getLayoutInflater().inflate(R.layout.gd_tab_indicator, tabWidget, false); + textIndicator.setText(label); + spec.setIndicator(textIndicator); + tabHost.addTab(spec); + } + + @SuppressWarnings("nls") + @Override + public void onTabChanged(String tabId) { + if(tabId.equals("tasks")) + findViewById(R.id.taskListFooter).setVisibility(View.VISIBLE); + else + findViewById(R.id.taskListFooter).setVisibility(View.GONE); + + if(tabId.equals("updates")) + findViewById(R.id.updatesFooter).setVisibility(View.VISIBLE); + else + findViewById(R.id.updatesFooter).setVisibility(View.GONE); + + if(tabId.equals("members")) + findViewById(R.id.membersFooter).setVisibility(View.VISIBLE); + else + findViewById(R.id.membersFooter).setVisibility(View.GONE); + } + + + /** + * Create options menu (displayed when user presses menu key) + * + * @return true if menu should be displayed + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if(menu.size() > 0) + menu.clear(); + + MenuItem item; + + item = menu.add(Menu.NONE, MENU_ADDONS_ID, Menu.NONE, + R.string.TLA_menu_addons); + item.setIcon(android.R.drawable.ic_menu_set_as); + + item = menu.add(Menu.NONE, MENU_SETTINGS_ID, Menu.NONE, + R.string.TLA_menu_settings); + item.setIcon(android.R.drawable.ic_menu_preferences); + + item = menu.add(Menu.NONE, MENU_SORT_ID, Menu.NONE, + R.string.TLA_menu_sort); + item.setIcon(android.R.drawable.ic_menu_sort_by_size); + + item = menu.add(Menu.NONE, MENU_REFRESH_ID, Menu.NONE, + R.string.actfm_TVA_menu_refresh); + item.setIcon(R.drawable.ic_menu_refresh); + + item = menu.add(Menu.NONE, MENU_HELP_ID, Menu.NONE, + R.string.TLA_menu_help); + item.setIcon(android.R.drawable.ic_menu_help); + + return true; + } + + protected void setUpMemberPage() { + tagMembers = (PeopleContainer) findViewById(R.id.members_container); + tagName = (EditText) findViewById(R.id.tag_name); + picture = (AsyncImageView) findViewById(R.id.picture); + + picture.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + ArrayAdapter adapter = new ArrayAdapter(TagViewActivity.this, + android.R.layout.simple_spinner_dropdown_item, new String[] { + getString(R.string.actfm_picture_camera), + getString(R.string.actfm_picture_gallery), + }); + + DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + @SuppressWarnings("nls") + @Override + public void onClick(DialogInterface d, int which) { + if(which == 0) { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + startActivityForResult(intent, REQUEST_CODE_CAMERA); + } else { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + startActivityForResult(Intent.createChooser(intent, + getString(R.string.actfm_TVA_tag_picture)), REQUEST_CODE_PICTURE); + } + } + }; + + // show a menu of available options + new AlertDialog.Builder(TagViewActivity.this) + .setAdapter(adapter, listener) + .show().setOwnerActivity(TagViewActivity.this); + } + }); + + findViewById(R.id.saveMembers).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + saveSettings(); + } + }); + + refreshMembersPage(); + } + + protected void setUpUpdateList() { + TodorooCursor currentCursor = tagDataService.getUpdates(tagData); + startManagingCursor(currentCursor); + + updateAdapter = new UpdateAdapter(this, R.layout.update_adapter_row, + currentCursor, false, null); + ((ListView)findViewById(R.id.tab_updates)).setAdapter(updateAdapter); + + final ImageButton quickAddButton = (ImageButton) findViewById(R.id.commentButton); + addCommentField = (EditText) findViewById(R.id.commentField); + addCommentField.setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { + if(actionId == EditorInfo.IME_NULL && addCommentField.getText().length() > 0) { + addComment(); + return true; + } + return false; + } + }); + addCommentField.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + quickAddButton.setVisibility((s.length() > 0) ? View.VISIBLE : View.GONE); + } + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // + } + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // + } + }); + quickAddButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + addComment(); + } + }); + } + + // --- data loading + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + synchronized(this) { + if(dataLoaded) + return; + dataLoaded = true; + } + + String tag = getIntent().getStringExtra(EXTRA_TAG_NAME); + long remoteId = getIntent().getLongExtra(EXTRA_TAG_REMOTE_ID, 0); + + if(tag == null && remoteId == 0) + return; + + TodorooCursor cursor = tagDataService.query(Query.select(TagData.PROPERTIES).where(Criterion.or(TagData.NAME.eq(tag), + Criterion.and(TagData.REMOTE_ID.gt(0), TagData.REMOTE_ID.eq(remoteId))))); + try { + tagData = new TagData(); + if(cursor.getCount() == 0) { + tagData.setValue(TagData.NAME, tag); + tagData.setValue(TagData.REMOTE_ID, remoteId); + tagDataService.save(tagData); + } else { + cursor.moveToFirst(); + tagData.readFromCursor(cursor); + } + } finally { + cursor.close(); + } + + String fetchKey = LAST_FETCH_KEY + tagData.getId(); + long lastFetchDate = Preferences.getLong(fetchKey, 0); + if(DateUtilities.now() > lastFetchDate + 300000L) { + refreshData(false, false); + Preferences.setLong(fetchKey, DateUtilities.now()); + } + + setUpUpdateList(); + setUpMemberPage(); + } + + private void refreshUpdatesList() { + Cursor cursor = updateAdapter.getCursor(); + cursor.requery(); + startManagingCursor(cursor); + } + + @Override + public void loadTaskListContent(boolean requery) { + super.loadTaskListContent(requery); + int count = taskAdapter.getCursor().getCount(); + + if(tagData != null && sortFlags <= SortHelper.FLAG_REVERSE_SORT && + count != tagData.getValue(TagData.TASK_COUNT)) { + tagData.setValue(TagData.TASK_COUNT, count); + tagDataService.save(tagData); + } + } + + @SuppressWarnings("nls") + private void refreshMembersPage() { + tagName.setText(tagData.getValue(TagData.NAME)); + picture.setUrl(tagData.getValue(TagData.PICTURE)); + + TextView ownerLabel = (TextView) findViewById(R.id.tag_owner); + try { + if(tagData.getFlag(TagData.FLAGS, TagData.FLAG_EMERGENT)) { + ownerLabel.setText(String.format("<%s>", getString(R.string.actfm_TVA_tag_owner_none))); + } else if(tagData.getValue(TagData.USER_ID) == 0) { + ownerLabel.setText(Preferences.getStringValue(ActFmPreferenceService.PREF_NAME)); + } else { + JSONObject owner = new JSONObject(tagData.getValue(TagData.USER)); + ownerLabel.setText(owner.getString("name")); + } + } catch (JSONException e) { + Log.e("tag-view-activity", "json error refresh owner", e); + ownerLabel.setText(""); + System.err.println(tagData.getValue(TagData.USER)); + } + + tagMembers.removeAllViews(); + String peopleJson = tagData.getValue(TagData.MEMBERS); + if(!TextUtils.isEmpty(peopleJson)) { + try { + JSONArray people = new JSONArray(peopleJson); + for(int i = 0; i < people.length(); i++) { + JSONObject person = people.getJSONObject(i); + TextView textView = null; + + if(person.has("id") && person.getLong("id") == ActFmPreferenceService.userId()) + textView = tagMembers.addPerson(Preferences.getStringValue(ActFmPreferenceService.PREF_NAME)); + else if(!TextUtils.isEmpty(person.optString("name"))) + textView = tagMembers.addPerson(person.getString("name")); + else if(!TextUtils.isEmpty(person.optString("email"))) + textView = tagMembers.addPerson(person.getString("email")); + + if(textView != null) { + textView.setTag(person); + textView.setEnabled(false); + } + } + } catch (JSONException e) { + System.err.println(peopleJson); + Log.e("tag-view-activity", "json error refresh members", e); + } + } + + tagMembers.addPerson(""); //$NON-NLS-1$ + } + + /** refresh the list with latest data from the web */ + private void refreshData(final boolean manual, boolean bypassTagShow) { + final boolean noRemoteId = tagData.getValue(TagData.REMOTE_ID) == 0; + + final ProgressDialog progressDialog; + if(manual && !noRemoteId) + progressDialog = DialogUtilities.progressDialog(this, getString(R.string.DLG_please_wait)); + else + progressDialog = null; + + Thread tagShowThread = new Thread(new Runnable() { + @SuppressWarnings("nls") + @Override + public void run() { + try { + String oldName = tagData.getValue(TagData.NAME); + actFmSyncService.fetchTag(tagData); + if(noRemoteId && tagData.getValue(TagData.REMOTE_ID) > 0) { + refreshData(manual, true); + + runOnUiThread(new Runnable() { + @Override + public void run() { + refreshUpdatesList(); + refreshMembersPage(); + } + }); + } + + if(!oldName.equals(tagData.getValue(TagData.NAME))) { + TagService.getInstance().rename(oldName, + tagData.getValue(TagData.NAME)); + } + + } catch (IOException e) { + Log.e("tag-view-activity", "error-fetching-task-io", e); + } catch (JSONException e) { + Log.e("tag-view-activity", "error-fetching-task", e); + } + } + }); + if(!bypassTagShow) + tagShowThread.start(); + + if(noRemoteId) + return; + + actFmSyncService.fetchTasksForTag(tagData, manual, new Runnable() { + @Override + public void run() { + runOnUiThread(new Runnable() { + @Override + public void run() { + loadTaskListContent(true); + DialogUtilities.dismissDialog(TagViewActivity.this, progressDialog); + } + }); + } + }); + + actFmSyncService.fetchUpdatesForTag(tagData, manual, new Runnable() { + @Override + public void run() { + runOnUiThread(new Runnable() { + @Override + public void run() { + refreshUpdatesList(); + DialogUtilities.dismissDialog(TagViewActivity.this, progressDialog); + } + }); + } + }); + } + + // --- receivers + + private final BroadcastReceiver notifyReceiver = new BroadcastReceiver() { + @SuppressWarnings("nls") + @Override + public void onReceive(Context context, Intent intent) { + System.err.println("TVA thug hustlin - "); + + if(!intent.hasExtra("tag_id")) + return; + System.err.println(Long.toString(tagData.getValue(TagData.REMOTE_ID)) + + " VS " + intent.getStringExtra("tag_id")); + if(!Long.toString(tagData.getValue(TagData.REMOTE_ID)).equals(intent.getStringExtra("tag_id"))) + return; + + runOnUiThread(new Runnable() { + @Override + public void run() { + System.err.println("REFRESH updates list pa-pa-pa"); + refreshUpdatesList(); + } + }); + refreshData(false, true); + + NotificationManager nm = new AndroidNotificationManager(ContextManager.getContext()); + nm.cancel(tagData.getValue(TagData.REMOTE_ID).intValue()); + } + }; + + @Override + protected void onResume() { + super.onResume(); + + IntentFilter intentFilter = new IntentFilter(BROADCAST_TAG_ACTIVITY); + registerReceiver(notifyReceiver, intentFilter); + } + + @Override + protected void onPause() { + super.onPause(); + + unregisterReceiver(notifyReceiver); + } + + // --- events + + private void saveSettings() { + String oldName = tagData.getValue(TagData.NAME); + String newName = tagName.getText().toString(); + + if(!oldName.equals(newName)) { + tagData.setValue(TagData.NAME, newName); + TagService.getInstance().rename(oldName, newName); + tagData.setFlag(TagData.FLAGS, TagData.FLAG_EMERGENT, false); + } + + JSONArray members = tagMembers.toJSONArray(); + tagData.setValue(TagData.MEMBERS, members.toString()); + tagData.setValue(TagData.MEMBER_COUNT, members.length()); + Flags.set(Flags.TOAST_ON_SAVE); + tagDataService.save(tagData); + + refreshMembersPage(); + } + + @SuppressWarnings("nls") + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if(requestCode == REQUEST_CODE_CAMERA && resultCode == RESULT_OK) { + Bitmap bitmap = data.getParcelableExtra("data"); + if(bitmap != null) { + picture.setImageBitmap(bitmap); + uploadTagPicture(bitmap); + } + } else if(requestCode == REQUEST_CODE_PICTURE && resultCode == RESULT_OK) { + Uri uri = data.getData(); + String[] projection = { MediaStore.Images.Media.DATA }; + Cursor cursor = managedQuery(uri, projection, null, null, null); + String path; + + if(cursor != null) { + try { + int column_index = cursor + .getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + path = cursor.getString(column_index); + } finally { + cursor.close(); + } + } else { + path = uri.getPath(); + } + + Bitmap bitmap = BitmapFactory.decodeFile(path); + if(bitmap != null) { + picture.setImageBitmap(bitmap); + uploadTagPicture(bitmap); + } + } + } + + private void uploadTagPicture(final Bitmap bitmap) { + new Thread(new Runnable() { + @Override + public void run() { + try { + String url = actFmSyncService.setTagPicture(tagData.getValue(TagData.REMOTE_ID), bitmap); + tagData.setValue(TagData.PICTURE, url); + Flags.set(Flags.SUPPRESS_SYNC); + tagDataService.save(tagData); + } catch (IOException e) { + DialogUtilities.okDialog(TagViewActivity.this, e.toString(), null); + } + } + }).start(); + } + + private void addComment() { + Update update = new Update(); + update.setValue(Update.MESSAGE, addCommentField.getText().toString()); + update.setValue(Update.ACTION_CODE, "tag_comment"); //$NON-NLS-1$ + update.setValue(Update.USER_ID, 0L); + update.setValue(Update.TAG, tagData.getId()); + update.setValue(Update.CREATION_DATE, DateUtilities.now()); + updateDao.createNew(update); + + addCommentField.setText(""); //$NON-NLS-1$ + refreshUpdatesList(); + } + + @Override + public boolean onMenuItemSelected(int featureId, final MenuItem item) { + // handle my own menus + switch (item.getItemId()) { + case MENU_REFRESH_ID: + refreshData(true, false); + return true; + } + + return super.onMenuItemSelected(featureId, item); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/TaskFields.java b/astrid/plugin-src/com/todoroo/astrid/actfm/TaskFields.java new file mode 100644 index 000000000..9a564891a --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/TaskFields.java @@ -0,0 +1,35 @@ +package com.todoroo.astrid.actfm; + +import com.todoroo.andlib.data.Property.IntegerProperty; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +import com.todoroo.astrid.data.Metadata; + +/** + * Metadata entry for a task shared with astrid.com + * + * @author Tim Su + * + */ +public class TaskFields { + + /** metadata key */ + public static final String METADATA_KEY = "actfm"; //$NON-NLS-1$ + + /** remote id*/ + public static final LongProperty REMOTE_ID = new LongProperty(Metadata.TABLE, Metadata.VALUE2.name); + + /** goal id */ + public static final LongProperty GOAL_ID = new LongProperty(Metadata.TABLE, Metadata.VALUE2.name); + + /** user id */ + public static final LongProperty USER_ID = new LongProperty(Metadata.TABLE, Metadata.VALUE3.name); + + /** user */ + public static final StringProperty USER = Metadata.VALUE4; + + /** comment count */ + public static final IntegerProperty COMMENT_COUNT = new IntegerProperty(Metadata.TABLE, + Metadata.VALUE4.name); + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java new file mode 100644 index 000000000..e3144bbe1 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java @@ -0,0 +1,203 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.actfm.sync; + +import java.util.ArrayList; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.ContentValues; +import android.content.Context; + +import com.todoroo.andlib.data.Property; +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.sql.Criterion; +import com.todoroo.andlib.sql.Join; +import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.dao.TaskDao.TaskCriteria; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.notes.NoteMetadata; +import com.todoroo.astrid.service.MetadataService; +import com.todoroo.astrid.service.StartupService; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.tags.TagService; + +public final class ActFmDataService { + + // --- constants + + /** Utility for joining tasks with metadata */ + public static final Join METADATA_JOIN = Join.left(Metadata.TABLE, Task.ID.eq(Metadata.TASK)); + + /** NoteMetadata provider string */ + public static final String NOTE_PROVIDER = "actfm-comment"; //$NON-NLS-1$ + + // --- instance variables + + protected final Context context; + + @Autowired TaskDao taskDao; + + @Autowired MetadataService metadataService; + + @Autowired ActFmPreferenceService actFmPreferenceService; + + @Autowired TagDataService tagDataService; + + public ActFmDataService() { + this.context = ContextManager.getContext(); + DependencyInjectionService.getInstance().inject(this); + } + + // --- task and metadata methods + + /** + * Clears metadata information. Used when user logs out of service + */ + public void clearMetadata() { + ContentValues values = new ContentValues(); + values.put(Task.REMOTE_ID.name, 0); + taskDao.updateMultiple(values, Criterion.all); + } + + /** + * Currently, this method does nothing, there is an alternate method to create tasks + * @param properties + * @return + */ + public TodorooCursor getLocallyCreated(Property[] properties) { + return taskDao.query(Query.select(properties).where(Criterion.and(TaskCriteria.isActive(), + Task.ID.gt(StartupService.INTRO_TASK_SIZE), + Task.REMOTE_ID.eq(0)))); + } + + /** + * Gets tasks that were modified since last sync + * @param properties + * @return null if never sync'd + */ + public TodorooCursor getLocallyUpdated(Property[] properties) { + long lastSyncDate = actFmPreferenceService.getLastSyncDate(); + if(lastSyncDate == 0) + return taskDao.query(Query.select(properties).where(Criterion.none)); + return + taskDao.query(Query.select(properties). + where(Criterion.and(Task.REMOTE_ID.gt(0), + Task.MODIFICATION_DATE.gt(lastSyncDate), + Task.MODIFICATION_DATE.gt(Task.LAST_SYNC))).groupBy(Task.ID)); + } + + /** + * Searches for a local task with same remote id, updates this task's id + * @param remoteTask + * @return true if found local match + */ + public boolean findLocalMatch(ActFmTaskContainer remoteTask) { + if(remoteTask.task.getId() != Task.NO_ID) + return true; + TodorooCursor cursor = taskDao.query(Query.select(Task.ID). + where(Task.REMOTE_ID.eq(remoteTask.task.getValue(Task.REMOTE_ID)))); + try { + if(cursor.getCount() == 0) + return false; + cursor.moveToFirst(); + remoteTask.task.setId(cursor.get(Task.ID)); + return true; + } finally { + cursor.close(); + } + } + + /** + * Saves a task and its metadata + * @param task + */ + public void saveTaskAndMetadata(ActFmTaskContainer task) { + taskDao.save(task.task); + + metadataService.synchronizeMetadata(task.task.getId(), task.metadata, + Criterion.or(Criterion.and(MetadataCriteria.withKey(NoteMetadata.METADATA_KEY), + NoteMetadata.EXT_PROVIDER.eq(NOTE_PROVIDER)), + MetadataCriteria.withKey(TagService.KEY))); + } + + /** + * Reads a task and its metadata + * @param task + * @return + */ + public ActFmTaskContainer readTaskAndMetadata(TodorooCursor taskCursor) { + Task task = new Task(taskCursor); + + // read tags, notes, etc + ArrayList metadata = new ArrayList(); + TodorooCursor metadataCursor = metadataService.query(Query.select(Metadata.PROPERTIES). + where(Criterion.and(MetadataCriteria.byTask(task.getId()), + Criterion.or(MetadataCriteria.withKey(TagService.KEY), + MetadataCriteria.withKey(NoteMetadata.METADATA_KEY))))); + try { + for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) { + metadata.add(new Metadata(metadataCursor)); + } + } finally { + metadataCursor.close(); + } + + return new ActFmTaskContainer(task, metadata); + } + + /** + * Reads task notes out of a task + */ + public TodorooCursor getTaskNotesCursor(long taskId) { + TodorooCursor cursor = metadataService.query(Query.select(Metadata.PROPERTIES). + where(MetadataCriteria.byTaskAndwithKey(taskId, NoteMetadata.METADATA_KEY))); + return cursor; + } + + /** + * Save / Merge JSON tagData + * @param tagObject + * @throws JSONException + */ + @SuppressWarnings("nls") + public void saveTagData(JSONObject tagObject) throws JSONException { + TodorooCursor cursor = tagDataService.query(Query.select(TagData.PROPERTIES).where( + Criterion.or(TagData.REMOTE_ID.eq(tagObject.get("id")), + Criterion.and(TagData.REMOTE_ID.eq(0), + TagData.NAME.eq(tagObject.getString("name")))))); + try { + cursor.moveToNext(); + TagData tagData = new TagData(); + if(!cursor.isAfterLast()) { + tagData.readFromCursor(cursor); + if(!tagData.getValue(TagData.NAME).equals(tagObject.getString("name"))) + TagService.getInstance().rename(tagData.getValue(TagData.NAME), tagObject.getString("name")); + cursor.moveToNext(); + } + ActFmSyncService.JsonHelper.tagFromJson(tagObject, tagData); + tagDataService.save(tagData); + + // delete the rest + + for(; !cursor.isAfterLast(); cursor.moveToNext()) { + tagData.readFromCursor(cursor); + if(!tagData.getValue(TagData.NAME).equals(tagObject.getString("name"))) + TagService.getInstance().rename(tagData.getValue(TagData.NAME), tagObject.getString("name")); + tagDataService.delete(tagData.getId()); + } + } finally { + cursor.close(); + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/sharing/ActFmInvoker.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmInvoker.java similarity index 62% rename from astrid/plugin-src/com/todoroo/astrid/sharing/ActFmInvoker.java rename to astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmInvoker.java index 11c47f0f1..e2e4ab2ed 100644 --- a/astrid/plugin-src/com/todoroo/astrid/sharing/ActFmInvoker.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmInvoker.java @@ -1,4 +1,4 @@ -package com.todoroo.astrid.sharing; +package com.todoroo.astrid.actfm.sync; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -9,21 +9,22 @@ import java.util.Collections; import java.util.Comparator; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.HttpEntity; import org.json.JSONException; import org.json.JSONObject; import android.util.Log; import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.RestClient; import com.todoroo.andlib.utility.Pair; -import com.todoroo.astrid.utility.Constants; @SuppressWarnings("nls") public class ActFmInvoker { /** NOTE: these values are development values & will not work on production */ - private static final String URL = "http://pre.act.fm/api"; + private static final String URL = "http://10.0.2.2:3000/api/"; private static final String APP_ID = "bf6170638298af8ed9a8c79995b1fc0f"; private static final String APP_SECRET = "e389bfc82a0d932332f9a8bd8203735f"; @@ -41,6 +42,7 @@ public class ActFmInvoker { */ public ActFmInvoker() { // + DependencyInjectionService.getInstance().inject(this); } /** @@ -49,10 +51,11 @@ public class ActFmInvoker { */ public ActFmInvoker(String token) { this.token = token; + DependencyInjectionService.getInstance().inject(this); } - public boolean hasToken() { - return token != null; + public String getToken() { + return token; } // --- special method invocations @@ -63,7 +66,7 @@ public class ActFmInvoker { public JSONObject authenticate(String email, String name, String provider, String secret) throws ActFmServiceException, IOException { JSONObject result = invoke( - "user_create", + "user_signin", "email", email, "name", name, "provider", provider, @@ -91,9 +94,38 @@ public class ActFmInvoker { ActFmServiceException { try { String request = createFetchUrl(method, getParameters); - if(Constants.DEBUG) - Log.e("act-fm-invoke", request); + Log.e("act-fm-invoke", request); String response = restClient.get(request); + Log.e("act-fm-invoke-response", response); + JSONObject object = new JSONObject(response); + if(object.getString("status").equals("error")) + throw new ActFmServiceException(object.getString("message")); + return object; + } catch (JSONException e) { + throw new IOException(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Invokes API method using HTTP POST + * + * @param method + * API method to invoke + * @param data + * data to transmit + * @param getParameters + * Name/Value pairs. Values will be URL encoded. + * @return response object + */ + public JSONObject post(String method, HttpEntity data, Object... getParameters) throws IOException, + ActFmServiceException { + try { + String request = createFetchUrl(method, getParameters); + Log.e("act-fm-post", request); + String response = restClient.post(request, data); + Log.e("act-fm-post-response", response); JSONObject object = new JSONObject(response); if(object.getString("status").equals("error")) throw new ActFmServiceException(object.getString("message")); @@ -115,15 +147,28 @@ public class ActFmInvoker { */ private String createFetchUrl(String method, Object... getParameters) throws UnsupportedEncodingException, NoSuchAlgorithmException { ArrayList> params = new ArrayList>(); - for(int i = 0; i < getParameters.length; i += 2) - params.add(new Pair(getParameters[i].toString(), getParameters[i+1])); + for(int i = 0; i < getParameters.length; i += 2) { + if(getParameters[i+1] instanceof ArrayList) { + ArrayList list = (ArrayList) getParameters[i+1]; + for(int j = 0; j < list.size(); j++) + params.add(new Pair(getParameters[i].toString() + "[]", + list.get(j))); + } else + params.add(new Pair(getParameters[i].toString(), getParameters[i+1])); + } params.add(new Pair("app_id", APP_ID)); + params.add(new Pair("time", System.currentTimeMillis() / 1000L)); + if(token != null) + params.add(new Pair("token", token)); Collections.sort(params, new Comparator>() { @Override public int compare(Pair object1, Pair object2) { - return object1.getLeft().compareTo(object2.getLeft()); + int result = object1.getLeft().compareTo(object2.getLeft()); + if(result == 0) + return object1.getRight().toString().compareTo(object2.getRight().toString()); + return result; } }); @@ -139,11 +184,11 @@ public class ActFmInvoker { requestBuilder.append(key).append('=').append(encoded).append('&'); - if(!key.endsWith("[]")) - sigBuilder.append(key).append(value); + sigBuilder.append(key).append(value); } sigBuilder.append(APP_SECRET); + System.err.println("SIG: " + sigBuilder); String signature = DigestUtils.md5Hex(sigBuilder.toString()); requestBuilder.append("sig").append('=').append(signature); return requestBuilder.toString(); diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java new file mode 100644 index 000000000..954d7e861 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java @@ -0,0 +1,98 @@ +package com.todoroo.astrid.actfm.sync; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.timsu.astrid.R; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.data.RemoteModel; +import com.todoroo.astrid.sync.SyncProviderUtilities; + +/** + * Methods for working with GTasks preferences + * + * @author timsu + * + */ +public class ActFmPreferenceService extends SyncProviderUtilities { + + /** add-on identifier */ + public static final String IDENTIFIER = "actfm"; //$NON-NLS-1$ + + @Override + public String getIdentifier() { + return IDENTIFIER; + } + + @Override + public int getSyncIntervalKey() { + return R.string.actfm_APr_interval_key; + } + + @Override + public void clearLastSyncDate() { + super.clearLastSyncDate(); + Preferences.setInt(ActFmPreferenceService.PREF_SERVER_TIME, 0); + } + + // --- user management + + /** + * @return get user id + */ + public static long userId() { + return Preferences.getLong(PREF_USER_ID, -1L); + } + + /** Act.fm current user id */ + public static final String PREF_USER_ID = IDENTIFIER + "_user"; //$NON-NLS-1$ + + /** Act.fm current user name */ + public static final String PREF_NAME = IDENTIFIER + "_name"; //$NON-NLS-1$ + + /** Act.fm current user picture */ + public static final String PREF_PICTURE = IDENTIFIER + "_picture"; //$NON-NLS-1$ + + /** Act.fm current user email */ + public static final String PREF_EMAIL = IDENTIFIER + "_email"; //$NON-NLS-1$ + + /** Act.fm last sync server time */ + public static final String PREF_SERVER_TIME = IDENTIFIER + "_time"; //$NON-NLS-1$ + + private static JSONObject user = null; + + /** + * Return JSON object user, either yourself or the user of the model + * @param update + * @return + */ + public static JSONObject userFromModel(RemoteModel model) { + if(model.getValue(RemoteModel.USER_ID_PROPERTY) == 0) + return thisUser(); + else { + try { + return new JSONObject(model.getValue(RemoteModel.USER_JSON_PROPERTY)); + } catch (JSONException e) { + return new JSONObject(); + } + } + } + + @SuppressWarnings("nls") + private synchronized static JSONObject thisUser() { + if(user == null) { + user = new JSONObject(); + try { + user.put("name", Preferences.getStringValue(PREF_NAME)); + user.put("email", Preferences.getStringValue(PREF_EMAIL)); + user.put("picture", Preferences.getStringValue(PREF_PICTURE)); + user.put("id", Preferences.getLong(PREF_USER_ID, 0)); + System.err.println(user); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return user; + } + +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/sharing/ActFmServiceException.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmServiceException.java similarity index 94% rename from astrid/plugin-src/com/todoroo/astrid/sharing/ActFmServiceException.java rename to astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmServiceException.java index 0ee0f54c5..0759a7b00 100644 --- a/astrid/plugin-src/com/todoroo/astrid/sharing/ActFmServiceException.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmServiceException.java @@ -1,4 +1,4 @@ -package com.todoroo.astrid.sharing; +package com.todoroo.astrid.actfm.sync; import java.io.IOException; diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncProvider.java new file mode 100644 index 000000000..706bd1e65 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncProvider.java @@ -0,0 +1,316 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.actfm.sync; + +import java.io.IOException; +import java.util.ArrayList; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Activity; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; + +import com.timsu.astrid.C2DMReceiver; +import com.timsu.astrid.R; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.actfm.ActFmBackgroundService; +import com.todoroo.astrid.actfm.ActFmLoginActivity; +import com.todoroo.astrid.actfm.ActFmPreferences; +import com.todoroo.astrid.actfm.sync.ActFmSyncService.JsonHelper; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.notes.NoteMetadata; +import com.todoroo.astrid.service.AstridDependencyInjector; +import com.todoroo.astrid.service.StatisticsService; +import com.todoroo.astrid.sync.SyncProvider; +import com.todoroo.astrid.sync.SyncProviderUtilities; +import com.todoroo.astrid.utility.Constants; + +@SuppressWarnings("nls") +public class ActFmSyncProvider extends SyncProvider { + + private ActFmInvoker invoker = null; + + @Autowired ActFmDataService actFmDataService; + @Autowired ActFmSyncService actFmSyncService; + @Autowired ActFmPreferenceService actFmPreferenceService; + + static { + AstridDependencyInjector.initialize(); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------ utility methods + // ---------------------------------------------------------------------- + + @Override + protected SyncProviderUtilities getUtilities() { + return actFmPreferenceService; + } + + /** + * Sign out of service, deleting all synchronization metadata + */ + public void signOut() { + actFmPreferenceService.setToken(null); + actFmPreferenceService.clearLastSyncDate(); + C2DMReceiver.unregister(); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------ initiating sync + // ---------------------------------------------------------------------- + + /** + * initiate sync in background + */ + @Override + protected void initiateBackground() { + try { + C2DMReceiver.register(); + String authToken = actFmPreferenceService.getToken(); + invoker = new ActFmInvoker(authToken); + + // check if we have a token & it works + if(authToken != null) { + performSync(); + } + } catch (IllegalStateException e) { + // occurs when application was closed + } catch (Exception e) { + handleException("actfm-authenticate", e, true); + } finally { + actFmPreferenceService.stopOngoing(); + } + } + + /** + * If user isn't already signed in, show sign in dialog. Else perform sync. + */ + @Override + protected void initiateManual(Activity activity) { + String authToken = actFmPreferenceService.getToken(); + actFmPreferenceService.stopOngoing(); + + // check if we have a token & it works + if(authToken == null) { + // display login-activity + Intent intent = new Intent(activity, ActFmLoginActivity.class); + activity.startActivityForResult(intent, 0); + } else { + activity.startService(new Intent(null, null, + activity, ActFmBackgroundService.class)); + } + } + + // ---------------------------------------------------------------------- + // ----------------------------------------------------- synchronization! + // ---------------------------------------------------------------------- + + protected void performSync() { + actFmPreferenceService.recordSyncStart(); + + try { + int serverTime = Preferences.getInt(ActFmPreferenceService.PREF_SERVER_TIME, 0); + ArrayList remoteTasks = new ArrayList(); + + serverTime = (int)(fetchRemoteTasks(serverTime, remoteTasks) - DateUtilities.now()/1000L); + + fetchRemoteTagData(serverTime); + + SyncData syncData = populateSyncData(remoteTasks); + + try { + synchronizeTasks(syncData); + } finally { + syncData.localCreated.close(); + syncData.localUpdated.close(); + } + + serverTime += DateUtilities.now()/1000L; + Preferences.setInt(ActFmPreferenceService.PREF_SERVER_TIME, serverTime); + actFmPreferenceService.recordSuccessfulSync(); + + StatisticsService.reportEvent("actfm-sync-finished"); //$NON-NLS-1$ + + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH); + ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + + } catch (IllegalStateException e) { + // occurs when application was closed + } catch (Exception e) { + handleException("actfm-sync", e, true); //$NON-NLS-1$ + } + } + + /** + * Read remote tag data and merge with local + * @param serverTime last sync time + */ + private void fetchRemoteTagData(int serverTime) throws ActFmServiceException, IOException, JSONException { + actFmSyncService.fetchTags(); + } + + /** + * Read remote task data into remote task array + * @param serverTime last sync time + */ + private int fetchRemoteTasks(int serverTime, + ArrayList remoteTasks) throws IOException, + ActFmServiceException, JSONException { + JSONObject result; + if(serverTime == 0) + result = invoker.invoke("task_list", "active", 1); + else + result = invoker.invoke("task_list", "modified_after", serverTime); + + JSONArray taskList = result.getJSONArray("list"); + for(int i = 0; i < taskList.length(); i++) { + ActFmTaskContainer remote = parseRemoteTask(taskList.getJSONObject(i)); + + // update reminder flags for incoming remote tasks to prevent annoying + if(remote.task.hasDueDate() && remote.task.getValue(Task.DUE_DATE) < DateUtilities.now()) + remote.task.setFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE, false); + + actFmDataService.findLocalMatch(remote); + + remoteTasks.add(remote); + } + return result.optInt("time", 0); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------------------ sync data + // ---------------------------------------------------------------------- + + /** + * Populate SyncData data structure + * @throws JSONException + */ + private SyncData populateSyncData(ArrayList remoteTasks) throws JSONException { + // fetch locally created tasks + TodorooCursor localCreated = actFmDataService.getLocallyCreated(Task.PROPERTIES); + + // fetch locally updated tasks + TodorooCursor localUpdated = actFmDataService.getLocallyUpdated(Task.PROPERTIES); + + return new SyncData(remoteTasks, localCreated, localUpdated); + } + + // ---------------------------------------------------------------------- + // ------------------------------------------------- create / push / pull + // ---------------------------------------------------------------------- + + @Override + protected ActFmTaskContainer create(ActFmTaskContainer local) throws IOException { + return push(local, null); + } + + /** Create a task container for the given remote task + * @throws JSONException */ + private ActFmTaskContainer parseRemoteTask(JSONObject remoteTask) throws JSONException { + Task task = new Task(); + ArrayList metadata = new ArrayList(); + + JsonHelper.taskFromJson(remoteTask, task, metadata); + ActFmTaskContainer container = new ActFmTaskContainer(task, metadata, remoteTask); + + return container; + } + + @Override + protected ActFmTaskContainer pull(ActFmTaskContainer task) throws IOException { + if(task.task.getValue(Task.REMOTE_ID) == 0) + throw new ActFmServiceException("Tried to read an invalid task"); //$NON-NLS-1$ + + JSONObject remote = invoker.invoke("task_show", "id", task.task.getValue(Task.REMOTE_ID)); + try { + return parseRemoteTask(remote); + } catch (JSONException e) { + throw new ActFmServiceException(e); + } + } + + /** + * Send changes for the given Task across the wire. + */ + @Override + protected ActFmTaskContainer push(ActFmTaskContainer local, ActFmTaskContainer remote) throws IOException { + long id = local.task.getValue(Task.REMOTE_ID); + + actFmSyncService.pushTaskOnSave(local.task, local.task.getDatabaseValues()); + + // push unsaved comments + for(Metadata item : local.metadata) { + if(NoteMetadata.METADATA_KEY.equals(item.getValue(Metadata.KEY))) + if(TextUtils.isEmpty(item.getValue(NoteMetadata.EXT_ID))) { + JSONObject comment = invoker.invoke("comment_add", + "task_id", id, + "message", item.getValue(NoteMetadata.BODY)); + item.setValue(NoteMetadata.EXT_ID, comment.optString("id")); + } + } + + return local; + } + + // ---------------------------------------------------------------------- + // --------------------------------------------------------- read / write + // ---------------------------------------------------------------------- + + @Override + protected ActFmTaskContainer read(TodorooCursor cursor) throws IOException { + return actFmDataService.readTaskAndMetadata(cursor); + } + + @Override + protected void write(ActFmTaskContainer task) throws IOException { + actFmDataService.saveTaskAndMetadata(task); + } + + // ---------------------------------------------------------------------- + // --------------------------------------------------------- misc helpers + // ---------------------------------------------------------------------- + + @Override + protected int matchTask(ArrayList tasks, ActFmTaskContainer target) { + int length = tasks.size(); + for(int i = 0; i < length; i++) { + ActFmTaskContainer task = tasks.get(i); + if (task.task.getValue(Task.REMOTE_ID) == target.task.getValue(Task.REMOTE_ID)) + return i; + } + return -1; + } + + @Override + protected int updateNotification(Context context, Notification notification) { + String notificationTitle = context.getString(R.string.actfm_notification_title); + Intent intent = new Intent(context, ActFmPreferences.class); + PendingIntent notificationIntent = PendingIntent.getActivity(context, 0, + intent, 0); + notification.setLatestEventInfo(context, + notificationTitle, context.getString(R.string.SyP_progress), + notificationIntent); + return Constants.NOTIFICATION_SYNC; + } + + @Override + protected void transferIdentifiers(ActFmTaskContainer source, + ActFmTaskContainer destination) { + destination.task.setValue(Task.REMOTE_ID, source.task.getValue(Task.REMOTE_ID)); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java new file mode 100644 index 000000000..7ab5722b0 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java @@ -0,0 +1,764 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.actfm.sync; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import org.apache.http.entity.mime.MultipartEntity; +import org.apache.http.entity.mime.content.ByteArrayBody; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.ContentValues; +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; + +import com.timsu.astrid.R; +import com.todoroo.andlib.data.AbstractModel; +import com.todoroo.andlib.data.DatabaseDao; +import com.todoroo.andlib.data.DatabaseDao.ModelUpdateListener; +import com.todoroo.andlib.data.Property.LongProperty; +import com.todoroo.andlib.data.Property.StringProperty; +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.sql.Order; +import com.todoroo.andlib.sql.Query; +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.dao.MetadataDao; +import com.todoroo.astrid.dao.TagDataDao; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.dao.UpdateDao; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria; +import com.todoroo.astrid.data.RemoteModel; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.data.Update; +import com.todoroo.astrid.service.MetadataService; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.service.TaskService; +import com.todoroo.astrid.tags.TagService; +import com.todoroo.astrid.utility.Flags; + +/** + * Service for synchronizing data on Astrid.com server with local. + * + * @author Tim Su + * + */ +@SuppressWarnings("nls") +public final class ActFmSyncService { + + // --- instance variables + + @Autowired TagDataService tagDataService; + @Autowired MetadataService metadataService; + @Autowired TaskService taskService; + @Autowired ActFmPreferenceService actFmPreferenceService; + @Autowired ActFmInvoker actFmInvoker; + @Autowired ActFmDataService actFmDataService; + @Autowired TaskDao taskDao; + @Autowired TagDataDao tagDataDao; + @Autowired UpdateDao updateDao; + @Autowired MetadataDao metadataDao; + + private String token; + + public ActFmSyncService() { + DependencyInjectionService.getInstance().inject(this); + } + + public void initialize() { + taskDao.addListener(new ModelUpdateListener() { + @Override + public void onModelUpdated(final Task model) { + if(Flags.checkAndClear(Flags.SUPPRESS_SYNC)) + return; + final ContentValues setValues = model.getSetValues(); + if(setValues == null || !checkForToken() || setValues.containsKey(RemoteModel.REMOTE_ID_PROPERTY_NAME)) + return; + + new Thread(new Runnable() { + @Override + public void run() { + // sleep so metadata associated with task is saved + AndroidUtilities.sleepDeep(1000L); + pushTaskOnSave(model, setValues); + } + }).start(); + } + }); + + updateDao.addListener(new ModelUpdateListener() { + @Override + public void onModelUpdated(final Update model) { + if(Flags.checkAndClear(Flags.SUPPRESS_SYNC)) + return; + final ContentValues setValues = model.getSetValues(); + if(setValues == null || !checkForToken() || model.getValue(Update.REMOTE_ID) > 0) + return; + + new Thread(new Runnable() { + @Override + public void run() { + pushUpdateOnSave(model, setValues); + } + }).start(); + } + }); + + tagDataDao.addListener(new ModelUpdateListener() { + @Override + public void onModelUpdated(final TagData model) { + if(Flags.checkAndClear(Flags.SUPPRESS_SYNC)) + return; + final ContentValues setValues = model.getSetValues(); + if(setValues == null || !checkForToken() || setValues.containsKey(RemoteModel.REMOTE_ID_PROPERTY_NAME)) + return; + + new Thread(new Runnable() { + @Override + public void run() { + pushTagDataOnSave(model, setValues); + } + }).start(); + } + }); + } + + // --- data push methods + + /** + * Synchronize with server when data changes + */ + public void pushUpdateOnSave(Update update, ContentValues values) { + if(!values.containsKey(Update.MESSAGE.name)) + return; + + ArrayList params = new ArrayList(); + params.add("message"); params.add(update.getValue(Update.MESSAGE)); + + if(update.getValue(Update.TAG) > 0) { + TagData tagData = tagDataService.fetchById(update.getValue(Update.TAG), TagData.REMOTE_ID); + if(tagData == null || tagData.getValue(TagData.REMOTE_ID) == 0) + return; + params.add("tag_id"); params.add(tagData.getValue(TagData.REMOTE_ID)); + } + + if(update.getValue(Update.TASK) > 0) { + Task task = taskService.fetchById(update.getValue(Update.TASK), Task.REMOTE_ID); + if(task == null || task.getValue(Task.REMOTE_ID) == 0) + return; + params.add("task"); params.add(task.getValue(Task.REMOTE_ID)); + } + if(!checkForToken()) + return; + + try { + params.add("token"); params.add(token); + JSONObject result = actFmInvoker.invoke("comment_add", params.toArray(new Object[params.size()])); + update.setValue(Update.REMOTE_ID, result.optLong("id")); + updateDao.saveExisting(update); + } catch (IOException e) { + handleException("task-save", e); + } + } + + /** + * Synchronize with server when data changes + */ + public void pushTaskOnSave(Task task, ContentValues values) { + long remoteId; + if(task.containsValue(Task.REMOTE_ID)) + remoteId = task.getValue(Task.REMOTE_ID); + else { + Task taskForRemote = taskService.fetchById(task.getId(), Task.REMOTE_ID); + if(taskForRemote == null) + return; + remoteId = taskForRemote.getValue(Task.REMOTE_ID); + } + boolean newlyCreated = remoteId == 0; + + ArrayList params = new ArrayList(); + + if(values.containsKey(Task.TITLE.name)) { + params.add("title"); params.add(task.getValue(Task.TITLE)); + } + if(values.containsKey(Task.DUE_DATE.name)) { + params.add("due"); params.add(task.getValue(Task.DUE_DATE) / 1000L); + params.add("has_due_time"); params.add(task.hasDueTime() ? 1 : 0); + } + if(values.containsKey(Task.NOTES.name)) { + params.add("notes"); params.add(task.getValue(Task.NOTES)); + } + if(values.containsKey(Task.DELETION_DATE.name)) { + params.add("deleted_at"); params.add(task.getValue(Task.DELETION_DATE) / 1000L); + } + if(values.containsKey(Task.COMPLETION_DATE.name)) { + params.add("completed"); params.add(task.getValue(Task.COMPLETION_DATE) / 1000L); + } + if(values.containsKey(Task.IMPORTANCE.name)) { + params.add("importance"); params.add(task.getValue(Task.IMPORTANCE)); + } + if(values.containsKey(Task.RECURRENCE.name)) { + params.add("repeat"); params.add(task.getValue(Task.RECURRENCE)); + } + if(values.containsKey(Task.USER_ID.name) && task.getValue(Task.USER_ID) >= 0) { + params.add("user_id"); + if(task.getValue(Task.USER_ID) == 0) + params.add(ActFmPreferenceService.userId()); + else + params.add(task.getValue(Task.USER_ID)); + } + if(Flags.checkAndClear(Flags.TAGS_CHANGED) || newlyCreated) { + TodorooCursor cursor = TagService.getInstance().getTags(task.getId()); + try { + if(cursor.getCount() == 0) { + params.add("tags"); + params.add(""); + } else { + Metadata metadata = new Metadata(); + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + metadata.readFromCursor(cursor); + if(metadata.containsNonNullValue(TagService.REMOTE_ID) && + metadata.getValue(TagService.REMOTE_ID) > 0) { + params.add("tag_ids[]"); + params.add(metadata.getValue(TagService.REMOTE_ID)); + } else { + params.add("tags[]"); + params.add(metadata.getValue(TagService.TAG)); + } + } + } + } finally { + cursor.close(); + } + } + + if(params.size() == 0 || !checkForToken()) + return; + + System.err.println("PUSHN ON SAVE: " + task.getMergedValues()); + System.err.println("SETVALUES: " + values); + + if(!newlyCreated) { + params.add("id"); params.add(remoteId); + } else if(!params.contains(Task.TITLE.name)) + return; + + try { + params.add("token"); params.add(token); + JSONObject result = actFmInvoker.invoke("task_save", params.toArray(new Object[params.size()])); + ArrayList metadata = new ArrayList(); + JsonHelper.taskFromJson(result, task, metadata); + task.setValue(Task.MODIFICATION_DATE, DateUtilities.now()); + task.setValue(Task.LAST_SYNC, DateUtilities.now()); + Flags.set(Flags.SUPPRESS_SYNC); + taskDao.saveExisting(task); + } catch (JSONException e) { + handleException("task-save-json", e); + } catch (IOException e) { + handleException("task-save-io", e); + } + } + + /** + * Synchronize complete task with server + * @param task + */ + public void pushTask(long taskId) { + Task task = taskService.fetchById(taskId, Task.PROPERTIES); + pushTaskOnSave(task, task.getMergedValues()); + } + + /** + * Send tagData changes to server + * @param setValues + */ + public void pushTagDataOnSave(TagData tagData, ContentValues values) { + long remoteId; + if(tagData.containsValue(TagData.REMOTE_ID)) + remoteId = tagData.getValue(TagData.REMOTE_ID); + else { + TagData forRemote = tagDataService.fetchById(tagData.getId(), TagData.REMOTE_ID); + if(forRemote == null) + return; + remoteId = forRemote.getValue(TagData.REMOTE_ID); + } + boolean newlyCreated = remoteId == 0; + + ArrayList params = new ArrayList(); + + if(values.containsKey(TagData.NAME.name)) { + params.add("name"); params.add(tagData.getValue(TagData.NAME)); + } + + if(values.containsKey(TagData.MEMBERS.name)) { + params.add("members"); + try { + JSONArray members = new JSONArray(tagData.getValue(TagData.MEMBERS)); + if(members.length() == 0) + params.add(""); + else { + ArrayList array = new ArrayList(members.length()); + for(int i = 0; i < members.length(); i++) { + JSONObject person = members.getJSONObject(i); + if(person.has("id")) + array.add(person.getLong("id")); + else { + if(person.has("name")) + array.add(person.getString("name") + " <" + + person.getString("email") + ">"); + else + array.add(person.getString("email")); + } + } + params.add(array); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + if(params.size() == 0 || !checkForToken()) + return; + + if(!newlyCreated) { + params.add("id"); params.add(remoteId); + } + + boolean success; + try { + params.add("token"); params.add(token); + JSONObject result = actFmInvoker.invoke("tag_save", params.toArray(new Object[params.size()])); + if(newlyCreated) { + tagData.setValue(TagData.REMOTE_ID, result.optLong("id")); + Flags.set(Flags.SUPPRESS_SYNC); + tagDataDao.saveExisting(tagData); + } + success = true; + } catch (IOException e) { + handleException("tag-save", e); + success = false; + } + if(!Flags.checkAndClear(Flags.TOAST_ON_SAVE)) + return; + + final boolean finalSuccess = success; + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + if(finalSuccess) + Toast.makeText(ContextManager.getContext(), + R.string.actfm_toast_success, Toast.LENGTH_LONG).show(); + else + Toast.makeText(ContextManager.getContext(), + R.string.actfm_toast_error, Toast.LENGTH_LONG).show(); + } + }); + } + + // --- data fetch methods + + /** + * Fetch tagData listing asynchronously + */ + public void fetchTagDataDashboard(boolean manual, final Runnable done) { + invokeFetchList("goal", manual, new ListItemProcessor() { + @Override + protected void mergeAndSave(JSONArray list, HashMap locals) throws JSONException { + TagData remote = new TagData(); + for(int i = 0; i < list.length(); i++) { + JSONObject item = list.getJSONObject(i); + readIds(locals, item, remote); + JsonHelper.tagFromJson(item, remote); + Flags.set(Flags.SUPPRESS_SYNC); + tagDataService.save(remote); + } + } + + @Override + protected HashMap getLocalModels() { + TodorooCursor cursor = tagDataService.query(Query.select(TagData.ID, + TagData.REMOTE_ID).where(TagData.REMOTE_ID.in(remoteIds)).orderBy( + Order.asc(TagData.REMOTE_ID))); + return cursorToMap(cursor, taskDao, TagData.REMOTE_ID, TagData.ID); + } + }, done, "goals"); + } + + /** + * Get details for this tag + * @param tagData + * @throws IOException + * @throws JSONException + */ + public void fetchTag(final TagData tagData) throws IOException, JSONException { + JSONObject result; + if(!checkForToken()) + return; + + if(tagData.getValue(TagData.REMOTE_ID) == 0) + result = actFmInvoker.invoke("tag_show", "name", tagData.getValue(TagData.NAME), + "token", token); + else + result = actFmInvoker.invoke("tag_show", "id", tagData.getValue(TagData.REMOTE_ID), + "token", token); + + JsonHelper.tagFromJson(result, tagData); + Flags.set(Flags.SUPPRESS_SYNC); + tagDataService.save(tagData); + } + + /** + * Fetch all tags + */ + public void fetchTags() throws JSONException, IOException { + if(!checkForToken()) + return; + + JSONObject result = actFmInvoker.invoke("tag_list", "token", token); + JSONArray tags = result.getJSONArray("list"); + for(int i = 0; i < tags.length(); i++) { + JSONObject tagObject = tags.getJSONObject(i); + actFmDataService.saveTagData(tagObject); + } + } + + /** + * Fetch tasks for the given tagData asynchronously + * @param tagData + * @param manual + * @param done + */ + public void fetchTasksForTag(final TagData tagData, final boolean manual, Runnable done) { + invokeFetchList("task", manual, new ListItemProcessor() { + @Override + protected void mergeAndSave(JSONArray list, HashMap locals) throws JSONException { + Task remote = new Task(); + + ArrayList metadata = new ArrayList(); + for(int i = 0; i < list.length(); i++) { + + JSONObject item = list.getJSONObject(i); + readIds(locals, item, remote); + JsonHelper.taskFromJson(item, remote, metadata); + + Flags.set(Flags.SUPPRESS_SYNC); + taskService.save(remote); + metadataService.synchronizeMetadata(remote.getId(), metadata, MetadataCriteria.withKey(TagService.KEY)); + } + + if(manual) { + for(Long localId : locals.values()) + taskDao.delete(localId); + } + } + + @Override + protected HashMap getLocalModels() { + TodorooCursor cursor = taskService.query(Query.select(Task.ID, + Task.REMOTE_ID).where(Task.REMOTE_ID.in(remoteIds)).orderBy( + Order.asc(Task.REMOTE_ID))); + return cursorToMap(cursor, taskDao, Task.REMOTE_ID, Task.ID); + } + }, done, "tasks:" + tagData.getId(), "tag_id", tagData.getValue(TagData.REMOTE_ID)); + } + + /** + * Fetch tasks for the given tagData asynchronously + * @param tagData + * @param manual + * @param done + */ + public void fetchUpdatesForTag(final TagData tagData, final boolean manual, Runnable done) { + invokeFetchList("activity", manual, new ListItemProcessor() { + @Override + protected void mergeAndSave(JSONArray list, HashMap locals) throws JSONException { + Update remote = new Update(); + for(int i = 0; i < list.length(); i++) { + JSONObject item = list.getJSONObject(i); + readIds(locals, item, remote); + JsonHelper.updateFromJson(item, tagData, remote); + + Flags.set(Flags.SUPPRESS_SYNC); + if(remote.getId() == AbstractModel.NO_ID) + updateDao.createNew(remote); + else + updateDao.saveExisting(remote); + } + } + + @Override + protected HashMap getLocalModels() { + TodorooCursor cursor = updateDao.query(Query.select(Update.ID, + Update.REMOTE_ID).where(Update.REMOTE_ID.in(remoteIds)).orderBy( + Order.asc(Update.REMOTE_ID))); + return cursorToMap(cursor, updateDao, Update.REMOTE_ID, Update.ID); + } + }, done, "updates:" + tagData.getId(), "tag_id", tagData.getValue(TagData.REMOTE_ID)); + } + + /** + * Update tag picture + * @param path + * @throws IOException + * @throws ActFmServiceException + */ + public String setTagPicture(long tagId, Bitmap bitmap) throws ActFmServiceException, IOException { + if(!checkForToken()) + return null; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if(bitmap.getWidth() > 512 || bitmap.getHeight() > 512) { + float scale = Math.min(512f / bitmap.getWidth(), 512f / bitmap.getHeight()); + bitmap = Bitmap.createScaledBitmap(bitmap, (int)(scale * bitmap.getWidth()), + (int)(scale * bitmap.getHeight()), false); + } + bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos); + byte[] bytes = baos.toByteArray(); + MultipartEntity data = new MultipartEntity(); + data.addPart("picture", new ByteArrayBody(bytes, "image/jpg", "image.jpg")); + JSONObject result = actFmInvoker.post("tag_set_picture", data, "id", tagId, "token", token); + return result.optString("url"); + } + + // --- generic invokation + + /** invoke authenticated method against the server */ + public JSONObject invoke(String method, Object... getParameters) throws IOException, + ActFmServiceException { + if(!checkForToken()) + throw new ActFmServiceException("not logged in"); + Object[] parameters = new Object[getParameters.length + 2]; + parameters[0] = "token"; + parameters[1] = token; + for(int i = 0; i < getParameters.length; i++) + parameters[i+2] = getParameters[i]; + return actFmInvoker.invoke(method, parameters); + } + + // --- helpers + + private abstract class ListItemProcessor { + protected Long[] remoteIds = null; + + abstract protected HashMap getLocalModels(); + + abstract protected void mergeAndSave(JSONArray list, + HashMap locals) throws JSONException; + + public void process(JSONArray list) throws JSONException { + readRemoteIds(list); + HashMap locals = getLocalModels(); + mergeAndSave(list, locals); + } + + + protected void readRemoteIds(JSONArray list) throws JSONException { + remoteIds = new Long[list.length()]; + for(int i = 0; i < list.length(); i++) + remoteIds[i] = list.getJSONObject(i).getLong("id"); + } + + protected void readIds(HashMap locals, JSONObject json, RemoteModel model) throws JSONException { + long remoteId = json.getLong("id"); + model.setValue(RemoteModel.REMOTE_ID_PROPERTY, remoteId); + if(locals.containsKey(remoteId)) { + model.setId(locals.remove(remoteId)); + } else { + model.clearValue(AbstractModel.ID_PROPERTY); + } + } + + protected HashMap cursorToMap(TodorooCursor cursor, DatabaseDao dao, + LongProperty remoteIdProperty, LongProperty localIdProperty) { + try { + HashMap map = new HashMap(cursor.getCount()); + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + long remoteId = cursor.get(remoteIdProperty); + long localId = cursor.get(localIdProperty); + + if(map.containsKey(remoteId)) + dao.delete(map.get(remoteId)); + map.put(remoteId, localId); + } + return map; + } finally { + cursor.close(); + } + } + + } + + /** Call sync method */ + private void invokeFetchList(final String model, final boolean manual, + final ListItemProcessor processor, final Runnable done, final String lastSyncKey, + Object... params) { + if(!checkForToken()) + return; + + long serverFetchTime = manual ? 0 : Preferences.getLong("actfm_time_" + lastSyncKey, 0); + final Object[] getParams = AndroidUtilities.concat(new Object[params.length + 4], params, "token", token, + "modified_after", serverFetchTime); + + new Thread(new Runnable() { + @Override + public void run() { + JSONObject result = null; + try { + result = actFmInvoker.invoke(model + "_list", getParams); + JSONArray list = result.getJSONArray("list"); + processor.process(list); + Preferences.setLong("actfm_time_" + lastSyncKey, result.optLong("time", 0)); + Preferences.setLong("actfm_last_" + lastSyncKey, DateUtilities.now()); + + if(done != null) + done.run(); + } catch (IOException e) { + handleException("io-exception-list-" + model, e); + } catch (JSONException e) { + handleException("json: " + result.toString(), e); + } + } + }).start(); + } + + protected void handleException(String message, Exception exception) { + Log.w("actfm-sync", message, exception); + } + + private boolean checkForToken() { + if(!actFmPreferenceService.isLoggedIn()) + return false; + token = actFmPreferenceService.getToken(); + return true; + } + + // --- json reader helper + + /** + * Read data models from JSON + */ + public static class JsonHelper { + + protected static long readDate(JSONObject item, String key) { + return item.optLong(key, 0) * 1000L; + } + + public static void updateFromJson(JSONObject json, TagData tagData, + Update model) throws JSONException { + model.setValue(Update.REMOTE_ID, json.getLong("id")); + readUser(json.getJSONObject("user"), model, Update.USER_ID, Update.USER); + model.setValue(Update.ACTION, json.getString("action")); + model.setValue(Update.ACTION_CODE, json.getString("action_code")); + model.setValue(Update.TARGET_NAME, json.getString("target_name")); + if(json.isNull("message")) + model.setValue(Update.MESSAGE, ""); + else + model.setValue(Update.MESSAGE, json.getString("message")); + model.setValue(Update.PICTURE, json.getString("picture")); + model.setValue(Update.CREATION_DATE, readDate(json, "created_at")); + model.setValue(Update.TAG, tagData.getId()); + } + + public static void readUser(JSONObject user, AbstractModel model, LongProperty idProperty, + StringProperty userProperty) throws JSONException { + long id = user.getLong("id"); + if(id == ActFmPreferenceService.userId()) { + model.setValue(idProperty, 0L); + if(userProperty != null) + model.setValue(userProperty, ""); + } else { + model.setValue(idProperty, id); + if(userProperty != null) + model.setValue(userProperty, user.toString()); + } + } + + /** + * Read tagData from JSON + * @param model + * @param json + * @throws JSONException + */ + public static void tagFromJson(JSONObject json, TagData model) throws JSONException { + model.clearValue(TagData.REMOTE_ID); + model.setValue(TagData.REMOTE_ID, json.getLong("id")); + model.setValue(TagData.NAME, json.getString("name")); + readUser(json.getJSONObject("user"), model, TagData.USER_ID, TagData.USER); + + if(json.has("picture")) + model.setValue(TagData.PICTURE, json.optString("picture", "")); + if(json.has("thumb")) + model.setValue(TagData.THUMB, json.optString("thumb", "")); + + if(json.has("is_silent")) + model.setFlag(TagData.FLAGS, TagData.FLAG_SILENT,json.getBoolean("is_silent")); + + if(json.has("emergent")) + model.setFlag(TagData.FLAGS, TagData.FLAG_EMERGENT,json.getBoolean("emergent")); + + if(json.has("members")) { + JSONArray members = json.getJSONArray("members"); + model.setValue(TagData.MEMBERS, members.toString()); + model.setValue(TagData.MEMBER_COUNT, members.length()); + } + + if(json.has("tasks")) + model.setValue(TagData.TASK_COUNT, json.getInt("tasks")); + } + + /** + * Read task from json + * @param json + * @param model + * @param metadata + * @throws JSONException + */ + public static void taskFromJson(JSONObject json, Task model, ArrayList metadata) throws JSONException { + metadata.clear(); + model.clearValue(Task.REMOTE_ID); + model.setValue(Task.REMOTE_ID, json.getLong("id")); + model.setValue(Task.FLAGS, 0); + readUser(json.getJSONObject("user"), model, Task.USER_ID, Task.USER); + readUser(json.getJSONObject("creator"), model, Task.CREATOR_ID, null); + model.setValue(Task.COMMENT_COUNT, json.getInt("comment_count")); + model.setValue(Task.TITLE, json.getString("title")); + model.setValue(Task.IMPORTANCE, json.getInt("importance")); + model.setValue(Task.DUE_DATE, + model.createDueDate(Task.URGENCY_SPECIFIC_DAY, readDate(json, "due"))); + model.setValue(Task.COMPLETION_DATE, readDate(json, "completed_at")); + model.setValue(Task.CREATION_DATE, readDate(json, "created_at")); + model.setValue(Task.DELETION_DATE, readDate(json, "deleted_at")); + model.setValue(Task.RECURRENCE, json.optString("repeat", "")); + model.setValue(Task.NOTES, json.optString("notes", "")); + model.setValue(Task.DETAILS_DATE, 0L); + + JSONArray tags = json.getJSONArray("tags"); + for(int i = 0; i < tags.length(); i++) { + JSONObject tag = tags.getJSONObject(i); + String name = tag.getString("name"); + Metadata tagMetadata = new Metadata(); + tagMetadata.setValue(Metadata.KEY, TagService.KEY); + tagMetadata.setValue(TagService.TAG, name); + tagMetadata.setValue(TagService.REMOTE_ID, tag.getLong("id")); + metadata.add(tagMetadata); + } + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmTaskContainer.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmTaskContainer.java new file mode 100644 index 000000000..8e2130f0c --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmTaskContainer.java @@ -0,0 +1,57 @@ +package com.todoroo.astrid.actfm.sync; + +import java.util.ArrayList; +import java.util.Date; + +import org.json.JSONObject; + +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.notes.NoteMetadata; +import com.todoroo.astrid.sync.SyncContainer; + +/** + * RTM Task Container + * + * @author Tim Su + * + */ +public class ActFmTaskContainer extends SyncContainer { + + public ActFmTaskContainer(Task task, ArrayList metadata) { + this.task = task; + this.metadata = metadata; + } + + @SuppressWarnings("nls") + public ActFmTaskContainer(Task task, ArrayList metadata, JSONObject remoteTask) { + this(task, metadata); + task.setValue(Task.REMOTE_ID, remoteTask.optLong("id")); + } + + /** create note metadata from comment json object */ + @SuppressWarnings("nls") + public static Metadata newNoteMetadata(JSONObject comment) { + Metadata metadata = new Metadata(); + metadata.setValue(Metadata.KEY, NoteMetadata.METADATA_KEY); + metadata.setValue(NoteMetadata.EXT_ID, comment.optString("id")); + metadata.setValue(NoteMetadata.EXT_PROVIDER, + ActFmDataService.NOTE_PROVIDER); + + Date creationDate = new Date(comment.optInt("date") * 1000L); + metadata.setValue(Metadata.CREATION_DATE, creationDate.getTime()); + metadata.setValue(NoteMetadata.BODY, comment.optString("message")); + + JSONObject owner = comment.optJSONObject("owner"); + metadata.setValue(NoteMetadata.THUMBNAIL, owner.optString("picture")); + String title = String.format("%s on %s", + owner.optString("name"), + DateUtilities.getDateString(ContextManager.getContext(), creationDate)); + metadata.setValue(NoteMetadata.TITLE, title); + + return metadata; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java b/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java index a216c1f04..32d6e781d 100644 --- a/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java +++ b/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java @@ -15,6 +15,7 @@ import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.res.Resources; import android.os.Handler; import android.text.TextUtils; @@ -31,6 +32,7 @@ import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.Task; @@ -41,7 +43,6 @@ import com.todoroo.astrid.legacy.LegacyTaskModel; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.tags.TagService; -import com.todoroo.astrid.utility.Flags; public class TasksXmlImporter { @@ -146,7 +147,8 @@ public class TasksXmlImporter { } } } finally { - Flags.set(Flags.REFRESH); + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH); + ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); handler.post(new Runnable() { @Override public void run() { @@ -504,7 +506,7 @@ public class TasksXmlImporter { String preferred = xpp.getAttributeValue(null, LegacyTaskModel.PREFERRED_DUE_DATE); if(preferred != null) { Date preferredDate = BackupDateUtilities.getDateFromIso8601String(value); - upgradeNotes = "Goal Deadline: " + + upgradeNotes = "Project Deadline: " + DateUtilities.getDateString(ContextManager.getContext(), preferredDate); } diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java b/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java index 302a26354..7947dd862 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java @@ -47,7 +47,7 @@ public final class CoreFilterExposer extends BroadcastReceiver { Filter recent = new Filter(r.getString(R.string.BFE_Recent), r.getString(R.string.BFE_Recent), new QueryTemplate().where( - Criterion.not(TaskCriteria.isReadOnly())).orderBy( + TaskCriteria.ownedByMe()).orderBy( Order.desc(Task.MODIFICATION_DATE)).limit(15), null); recent.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.tango_new)).getBitmap(); @@ -69,8 +69,7 @@ public final class CoreFilterExposer extends BroadcastReceiver { public static Filter buildInboxFilter(Resources r) { Filter inbox = new Filter(r.getString(R.string.BFE_Active), r.getString(R.string.BFE_Active), new QueryTemplate().where( - Criterion.and(TaskCriteria.isActive(), TaskCriteria.isVisible(), - Criterion.not(TaskCriteria.isReadOnly()), + Criterion.and(TaskCriteria.activeVisibleMine(), Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).where( Criterion.and(MetadataCriteria.withKey(TagService.KEY), TagService.TAG.like("x_%", "x"))))))), //$NON-NLS-1$ //$NON-NLS-2$ diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java index e0bac43da..b3a70a080 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java @@ -184,7 +184,7 @@ public class CustomFilterActivity extends ListActivity { getString(R.string.CFC_dueBefore_text), Query.select(Task.ID).from(Task.TABLE).where( Criterion.and( - TaskCriteria.activeAndVisible(), + TaskCriteria.activeVisibleMine(), Criterion.or( Field.field("?").eq(0), Task.DUE_DATE.gt(0)), @@ -212,7 +212,7 @@ public class CustomFilterActivity extends ListActivity { IDENTIFIER_IMPORTANCE, getString(R.string.CFC_importance_text), Query.select(Task.ID).from(Task.TABLE).where( - Criterion.and(TaskCriteria.activeAndVisible(), + Criterion.and(TaskCriteria.activeVisibleMine(), Task.IMPORTANCE.lte("?"))).toString(), values, entries, entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_warning)).getBitmap(), @@ -228,7 +228,7 @@ public class CustomFilterActivity extends ListActivity { IDENTIFIER_TITLE, getString(R.string.CFC_title_contains_text), Query.select(Task.ID).from(Task.TABLE).where( - Criterion.and(TaskCriteria.activeAndVisible(), + Criterion.and(TaskCriteria.activeVisibleMine(), Task.TITLE.like("%?%"))).toString(), null, getString(R.string.CFC_title_contains_name), "", ((BitmapDrawable)r.getDrawable(R.drawable.tango_alpha)).getBitmap(), @@ -371,9 +371,9 @@ public class CustomFilterActivity extends ListActivity { // special code for all tasks universe if(instance.criterion.sql == null) - sql.append(TaskCriteria.activeAndVisible()).append(' '); + sql.append(TaskCriteria.activeVisibleMine()).append(' '); else { - String subSql = instance.criterion.sql.replace("?", value); + String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value)); sql.append(Task.ID).append(" IN (").append(subSql).append(") "); } @@ -434,7 +434,7 @@ public class CustomFilterActivity extends ListActivity { // special code for all tasks universe if(instance.criterion.sql == null) - sql.append(TaskCriteria.activeAndVisible()).append(' '); + sql.append(TaskCriteria.activeVisibleMine()).append(' '); else { String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value)); subSql = PermaSql.replacePlaceholders(subSql); diff --git a/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java b/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java index 790f7ffed..3b2bdd9e0 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java @@ -12,6 +12,7 @@ import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.service.AddOnService; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.MetadataService; +import com.todoroo.astrid.service.TagDataService; import com.todoroo.astrid.service.TaskService; /** @@ -37,6 +38,9 @@ public final class PluginServices { @Autowired AddOnService addOnService; + @Autowired + TagDataService tagDataService; + @Autowired StoreObjectDao storeObjectDao; @@ -61,6 +65,10 @@ public final class PluginServices { return getInstance().taskService; } + public static TagDataService getProjectService() { + return getInstance().tagDataService; + } + public static ExceptionService getExceptionService() { return getInstance().exceptionService; } @@ -87,9 +95,10 @@ public final class PluginServices { TodorooCursor cursor = PluginServices.getMetadataService().query(Query.select( Metadata.PROPERTIES).where(MetadataCriteria.byTaskAndwithKey(taskId, metadataKey))); try { - if(cursor.getCount() > 0) + if(cursor.getCount() > 0) { + cursor.moveToNext(); return new Metadata(cursor); - else + } else return null; } finally { cursor.close(); diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java index fb9ed8638..335eb8b09 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java @@ -27,11 +27,8 @@ import com.todoroo.andlib.data.Property; 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.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; -import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.core.PluginServices; @@ -51,6 +48,7 @@ import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncContainer; import com.todoroo.astrid.sync.SyncProvider; +import com.todoroo.astrid.sync.SyncProviderUtilities; import com.todoroo.astrid.utility.Constants; import com.todoroo.gtasks.GoogleConnectionManager; import com.todoroo.gtasks.GoogleLoginException; @@ -90,17 +88,15 @@ public class GtasksSyncProvider extends SyncProvider { AstridDependencyInjector.initialize(); } - @Autowired protected ExceptionService exceptionService; - - public GtasksSyncProvider() { - super(); - DependencyInjectionService.getInstance().inject(this); - } - // ---------------------------------------------------------------------- // ------------------------------------------------------ utility methods // ---------------------------------------------------------------------- + @Override + protected SyncProviderUtilities getUtilities() { + return gtasksPreferenceService; + } + /** * Sign out of service, deleting all synchronization metadata */ @@ -111,44 +107,6 @@ public class GtasksSyncProvider extends SyncProvider { gtasksMetadataService.clearMetadata(); } - /** - * Deal with a synchronization exception. If requested, will show an error - * to the user (unless synchronization is happening in background) - * - * @param context - * @param tag - * error tag - * @param e - * exception - * @param showError - * whether to display a dialog - */ - @Override - protected void handleException(String tag, Exception e, boolean displayError) { - final Context context = ContextManager.getContext(); - gtasksPreferenceService.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 GoogleTasksException) && e instanceof IOException) { - message = context.getString(R.string.SyP_ioerror); - exceptionService.reportError(tag + "-ioexception", e); //$NON-NLS-1$ - } 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); - } - } - // ---------------------------------------------------------------------- // ------------------------------------------------------ initiating sync // ---------------------------------------------------------------------- @@ -444,11 +402,10 @@ public class GtasksSyncProvider extends SyncProvider { TaskCreator createdTask = l.createTask(local.task.getValue(Task.TITLE)); createdTask.parentId(local.parentId); updateTaskHelper(local, null, createdTask); + return local; } catch (JSONException e) { throw new GoogleTasksException(e); } - - return local; } private void updateTaskHelper(final GtasksTaskContainer local, @@ -559,7 +516,7 @@ public class GtasksSyncProvider extends SyncProvider { * have changed. */ @Override - protected void push(GtasksTaskContainer local, GtasksTaskContainer remote) throws IOException { + protected GtasksTaskContainer push(GtasksTaskContainer local, GtasksTaskContainer remote) throws IOException { try { gtasksTaskListUpdater.updateParentAndSibling(local); @@ -574,6 +531,8 @@ public class GtasksSyncProvider extends SyncProvider { } catch (JSONException e) { throw new GoogleTasksException(e); } + + return pull(remote); } // ---------------------------------------------------------------------- diff --git a/astrid/plugin-src/com/todoroo/astrid/notes/NoteViewingActivity.java b/astrid/plugin-src/com/todoroo/astrid/notes/NoteViewingActivity.java index 81ac29b3d..4442d345c 100644 --- a/astrid/plugin-src/com/todoroo/astrid/notes/NoteViewingActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/notes/NoteViewingActivity.java @@ -90,4 +90,4 @@ public class NoteViewingActivity extends Activity { }); body.addView(ok); } -} \ No newline at end of file +} diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevRestClient.java b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevRestClient.java index f4686ac8d..c1e1dd199 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevRestClient.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/api/ProducteevRestClient.java @@ -10,7 +10,6 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; @@ -151,7 +150,7 @@ public class ProducteevRestClient implements RestClient { * url-encoded data * @throws IOException */ - public synchronized String post(String url, String data) throws IOException { + public synchronized String post(String url, HttpEntity data) throws IOException { initializeHttpClient(); if(Constants.DEBUG) @@ -159,7 +158,7 @@ public class ProducteevRestClient implements RestClient { try { HttpPost httpPost = new HttpPost(url); - httpPost.setEntity(new StringEntity(data)); + httpPost.setEntity(data); HttpResponse response = httpClient.execute(httpPost); return processHttpResponse(response); diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java index 0b342b543..010c97586 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java @@ -83,10 +83,10 @@ public final class ProducteevDataService { * Clears metadata information. Used when user logs out of service */ public void clearMetadata() { - metadataService.deleteWhere(Metadata.KEY.eq(ProducteevTask.METADATA_KEY)); - storeObjectDao.deleteWhere(StoreObject.TYPE.eq(ProducteevDashboard.TYPE)); PluginServices.getTaskService().clearDetails(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE). where(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY)))); + metadataService.deleteWhere(Metadata.KEY.eq(ProducteevTask.METADATA_KEY)); + storeObjectDao.deleteWhere(StoreObject.TYPE.eq(ProducteevDashboard.TYPE)); } /** diff --git a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java index 81a61860f..c438b5f51 100644 --- a/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevSyncProvider.java @@ -29,7 +29,6 @@ import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.service.NotificationManager; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; -import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.activity.ShortcutActivity; import com.todoroo.astrid.api.AstridApiConstants; @@ -50,9 +49,9 @@ import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncContainer; import com.todoroo.astrid.sync.SyncProvider; +import com.todoroo.astrid.sync.SyncProviderUtilities; import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.utility.Constants; -import com.todoroo.astrid.utility.Flags; @SuppressWarnings("nls") public class ProducteevSyncProvider extends SyncProvider { @@ -100,41 +99,9 @@ public class ProducteevSyncProvider extends SyncProvider