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 extends AbstractModel> 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 extends AbstractModel> 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 extends AbstractModel> 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