Astrid Collaboration Project

- sync with Astrid.com server
- people activity for delegating and sharing tasks
- shared tags activity for adding users to tags
- c2dm push notifications
pull/14/head
Tim Su 13 years ago
parent 5fd62cb8f9
commit 26c2d3c49b

3
.gitignore vendored

@ -11,4 +11,5 @@ release
dev
lp-translations/
translations/strings.xml
.DS_Store
greendroid/GDCatalog/.project

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -76,6 +76,12 @@
<!-- plurals: tasks -->
<item quantity="other">%d tasks</item>
</plurals>
<plurals name="Npeople">
<!-- plurals: people -->
<item quantity="one">1 person</item>
<!-- plurals: people -->
<item quantity="other">%d people</item>
</plurals>
<!-- ================================================== Generic Dialogs == -->

@ -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<DatabaseUpdateListener> listeners = new ArrayList<DatabaseUpdateListener>();
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

@ -30,7 +30,7 @@ import com.todoroo.andlib.data.Property.PropertyVisitor;
* @author Tim Su <tim@todoroo.com>
*
*/
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);

@ -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<TYPE extends AbstractModel> {
table = database.getTable(modelClass);
}
// --- listeners
public interface ModelUpdateListener<MTYPE> {
public void onModelUpdated(MTYPE model);
}
private final ArrayList<ModelUpdateListener<TYPE>> listeners =
new ArrayList<ModelUpdateListener<TYPE>>();
public void addListener(ModelUpdateListener<TYPE> listener) {
listeners.add(listener);
}
protected void onModelUpdated(TYPE model) {
for(ModelUpdateListener<TYPE> listener : listeners) {
listener.onModelUpdated(model);
}
}
// --- dao methods
/**
@ -207,12 +227,14 @@ public class DatabaseDao<TYPE extends AbstractModel> {
* @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<TYPE extends AbstractModel> {
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;
}

@ -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);

@ -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;
}

@ -83,7 +83,7 @@ public class Field extends DBObject<Field> {
return UnaryCriterion.like(this, value, escape);
}
public <T> Criterion in(final T... value) {
public <T> Criterion in(final T[] value) {
final Field field = this;
return new Criterion(Operator.in) {

@ -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> 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;
}
}

@ -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<String, Uri> cache = new HashMap<String, Uri>();
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<PhotoToLoad> photosToLoad = new Stack<PhotoToLoad>();
// 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();
}
}

@ -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() {

@ -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 <tim@todoroo.com>
*
*/
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);
}

@ -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 <tim@todoroo.com>
*
*/
@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<TagData> cursor) {
this();
readPropertiesFromCursor(cursor);
}
public void readFromCursor(TodorooCursor<TagData> cursor) {
super.readPropertiesFromCursor(cursor);
}
@Override
public long getId() {
return getIdHelper(ID);
}
// --- parcelable helpers
public static final Creator<TagData> CREATOR = new ModelCreator<TagData>(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;
}
}

@ -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 */

@ -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 <tim@todoroo.com>
*
*/
@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<Update> cursor) {
this();
readPropertiesFromCursor(cursor);
}
public void readFromCursor(TodorooCursor<Update> cursor) {
super.readPropertiesFromCursor(cursor);
}
@Override
public long getId() {
return getIdHelper(ID);
};
// --- parcelable helpers
private static final Creator<Update> CREATOR = new ModelCreator<Update>(Update.class);
@Override
protected Creator<? extends AbstractModel> getCreator() {
return CREATOR;
}
}

@ -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 <tim@todoroo.com>
*
*/
@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<User> cursor) {
this();
readPropertiesFromCursor(cursor);
}
public void readFromCursor(TodorooCursor<User> cursor) {
super.readPropertiesFromCursor(cursor);
}
@Override
public long getId() {
return getIdHelper(ID);
}
// --- parcelable helpers
public static final Creator<User> CREATOR = new ModelCreator<User>(User.class);
@Override
protected Creator<? extends AbstractModel> getCreator() {
return CREATOR;
}
}

@ -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<TYPE extends SyncContainer> {
// --- abstract methods - your services should implement these
/**
* @return sync utility instance
*/
abstract protected SyncProviderUtilities getUtilities();
/**
* Perform log in (launching activity if necessary) and sync. This is
* invoked when users manually request synchronization
@ -65,36 +75,25 @@ public abstract class SyncProvider<TYPE extends SyncContainer> {
*/
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<TYPE extends SyncContainer> {
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<TYPE extends SyncContainer> {
int remoteIndex = matchTask((ArrayList<TYPE>)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 extends SyncContainer> {
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<TYPE extends SyncContainer> {
}
}
// --- 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 */

@ -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);

@ -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();
}

@ -3,7 +3,7 @@
<classpathentry excluding="javax/xml/validation/|javax/xml/transform/dom/|javax/xml/parsers/|org/jaxp/transform/dom/|org/jaxp/transform/sax/|org/jaxp/transform/stax/|org/jaxp/transform/stream/|org/jaxp/stream/events/|org/jaxp/stream/util/|org/jaxp/parsers/|org/jaxp/stream/|org/jaxp/validation/" kind="src" path="src"/>
<classpathentry kind="src" path="src-legacy"/>
<classpathentry kind="src" path="common-src"/>
<classpathentry excluding="com/todoroo/astrid/rmilk/EditOperationExposer.java|com/todoroo/astrid/rmilk/MilkEditActivity.java" kind="src" path="plugin-src"/>
<classpathentry excluding="com/todoroo/astrid/rmilk/EditOperationExposer.java|com/todoroo/astrid/rmilk/MilkEditActivity.java|com/todoroo/astrid/actfm/TaskFields.java|com/todoroo/astrid/actfm/ShowProjectExposer.java|com/todoroo/astrid/actfm/ProjectDetailExposer.java|com/todoroo/astrid/actfm/ProjectListActivity.java" kind="src" path="plugin-src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="rmilk-src"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
@ -15,6 +15,7 @@
<classpathentry exported="true" kind="lib" path="libs/locale_platform.jar"/>
<classpathentry exported="true" kind="lib" path="libs/todoroo-g.jar"/>
<classpathentry kind="lib" path="libs/framework.jar"/>
<classpathentry kind="lib" path="libs/httpmime-4.1.1.jar"/>
<classpathentry kind="src" path="astridApi_src"/>
<classpathentry kind="src" path="facebook_src"/>
<classpathentry kind="src" path="GreenDroid_src"/>

@ -25,6 +25,10 @@
<!-- for google tasks -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
<!-- for task sharing -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- for push notifications -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- ============================================== Exported Permissions = -->
@ -49,6 +53,10 @@
android:protectionLevel="normal"
android:label="@string/write_permission_label" />
<uses-permission android:name="com.todoroo.astrid.WRITE" />
<!-- for receiving C2D messages-->
<permission android:name="com.timsu.astrid.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="com.timsu.astrid.permission.C2D_MESSAGE" />
<!-- ========================================================== Metadata = -->
@ -57,7 +65,8 @@
<supports-screens />
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:theme="@style/Theme">
android:theme="@style/Theme"
android:name="greendroid.app.GDApplication">
<!-- ====================================================== Activities = -->
@ -137,7 +146,7 @@
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<!-- ======================================================= Receivers = -->
<receiver android:name="com.todoroo.astrid.reminders.Notifications" />
@ -386,7 +395,63 @@
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter>
</receiver>
<!-- actfm -->
<activity android:name="com.todoroo.astrid.actfm.ActFmLoginActivity">
</activity>
<activity android:name="com.todoroo.astrid.actfm.ActFmPreferences"
android:theme="@android:style/Theme"
android:label="@string/actfm_APr_header">
<meta-data android:name="category"
android:resource="@string/SyP_label" />
<intent-filter>
<action android:name="com.todoroo.astrid.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name="com.todoroo.astrid.actfm.ActFmBackgroundService">
<intent-filter>
<action android:name="com.todoroo.astrid.actfm.SYNC" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<receiver android:name="com.todoroo.astrid.actfm.ActFmSyncActionExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_SYNC_ACTIONS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<activity android:name="com.todoroo.astrid.actfm.TagViewActivity"
android:windowSoftInputMode="stateHidden"
android:theme="@style/Theme" />
<activity android:name="com.todoroo.astrid.actfm.EditPeopleActivity"
android:windowSoftInputMode="stateHidden"
android:theme="@style/Theme">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<receiver android:name="com.todoroo.astrid.actfm.EditPeopleExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_ACTIONS" />
<action android:name="com.todoroo.astrid.EDIT_PEOPLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.timsu.astrid.C2DMReceiver" permission="com.google.android.c2dm.permission.SEND">
<!-- Receive the actual message -->
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.timsu.astrid" />
</intent-filter>
<!-- Receive the registration id -->
<intent-filter>
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="com.timsu.astrid" />
</intent-filter>
</receiver>
<!-- timers -->
<receiver android:name="com.todoroo.astrid.timers.TimerActionExposer">
<intent-filter>

Binary file not shown.

@ -6,8 +6,7 @@
<intAttribute key="ch.zork.quicklaunch.index" value="0"/>
<stringAttribute key="ch.zork.quicklaunch.mode" value="run"/>
<intAttribute key="com.android.ide.eclipse.adt.action" value="0"/>
<stringAttribute key="com.android.ide.eclipse.adt.activity" value="com.todoroo.astrid.sharing.SharingLoginActivity"/>
<stringAttribute key="com.android.ide.eclipse.adt.avd" value="android-23-wvga800"/>
<stringAttribute key="com.android.ide.eclipse.adt.activity" value="com.todoroo.astrid.activity.FilterListActivity"/>
<stringAttribute key="com.android.ide.eclipse.adt.commandline" value="-scale 0.7"/>
<intAttribute key="com.android.ide.eclipse.adt.delay" value="0"/>
<booleanAttribute key="com.android.ide.eclipse.adt.nobootanim" value="true"/>
@ -16,9 +15,11 @@
<booleanAttribute key="com.android.ide.eclipse.adt.wipedata" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/astrid"/>
<listEntry value="/astrid/AndroidManifest.xml"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
<listEntry value="1"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>

@ -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

@ -111,7 +111,7 @@
</target>
<!-- post build: rename apk -->
<target name="release" depends="clean, get-version, warnings, android_rules.release">
<target name="release" depends="clean, get-version, warnings, updatekeys, android_rules.release">
<mkdir dir="../release" />
<property name="out.final.package"
location="../release/${ant.project.name}-${manifest.version.code}-${manifest.version.name}-release.apk" />
@ -125,6 +125,23 @@
<echo>Final Release Package: ${out.final.package}</echo>
</target>
<!-- update api keys -->
<target name="updatekeys" description="update api key values">
<property file="${apikey.keyfile}" />
<replaceregexp file="${source.dir}/com/todoroo/astrid/sharing/sync/ActFmInvoker.java"
match="URL = .*"
replace="URL = &quot;${apikey.actfm.url}&quot;;" />
<replaceregexp file="${source.dir}/com/todoroo/astrid/sharing/sync/ActFmInvoker.java"
match="APP_ID = .*"
replace="APP_ID = &quot;${apikey.actfm.id}&quot;;" />
<replaceregexp file="${source.dir}/com/todoroo/astrid/sharing/sync/ActFmInvoker.java"
match="APP_SECRET = .*"
replace="APP_SECRET = &quot;${apikey.actfm.secret}&quot;;" />
<replaceregexp file="${source.dir}/com/todoroo/astrid/sharing/SharingLoginActivity.java"
match="APP_ID = .*"
replace="APP_ID = &quot;${apikey.facebook.id}&quot;;" />
</target>
<!-- jar signing -->
<target name="signjar" description="signs the jar">
<fail unless="source" message="need {source} to sign (jar base name)" />

@ -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());
}
}

Binary file not shown.

@ -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<TagData> 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);
}
}

@ -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();
}
}

@ -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));
}
}

@ -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();
}
}

@ -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);
}
}

@ -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<Metadata> nonSharedTags = new ArrayList<Metadata>();
@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<AssignedToUser> spinnerValues = new ArrayList<AssignedToUser>();
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<JSONObject> sharedPeople = new ArrayList<JSONObject>();
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<Metadata> 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<JSONObject> sharedPeople) throws JSONException {
HashSet<Long> userIds = new HashSet<Long>();
HashSet<String> emails = new HashSet<String>();
HashMap<String, AssignedToUser> names = new HashMap<String, AssignedToUser>();
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<AssignedToUser> usersAdapter = new ArrayAdapter<AssignedToUser>(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> metadata = new ArrayList<Metadata>(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> 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> 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> metadata = new ArrayList<Metadata>();
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<Metadata>
tags) throws JSONException {
ArrayList<Object> values = new ArrayList<Object>();
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);
}
}

@ -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 <tim@todoroo.com>
*
*/
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);
}
}
}

@ -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 <tim@todoroo.com>
*
*/
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 "<img src='silk_world'/> Public"; //$NON-NLS-1$
return null;
// return "<img src='silk_group'/> " + tagData.getValue(TagData.TITLE); //$NON-NLS-1$
}
}

@ -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 <tim@todoroo.com>
*
*/
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<String> queryTemplate = new AtomicReference<String>();
/* ======================================================================
* ======================================================= 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<TagData> 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;
}
}

@ -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);
}
}
}

@ -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<String> adapter = new ArrayAdapter<String>(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<Update> 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<TagData> 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("<error>");
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);
}
}

@ -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 <tim@todoroo.com>
*
*/
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);
}

@ -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<Task> 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<Task> 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<Task> 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<Task> taskCursor) {
Task task = new Task(taskCursor);
// read tags, notes, etc
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
TodorooCursor<Metadata> 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<Metadata> getTaskNotesCursor(long taskId) {
TodorooCursor<Metadata> 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<TagData> 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();
}
}
}

@ -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<Pair<String, Object>> params = new ArrayList<Pair<String, Object>>();
for(int i = 0; i < getParameters.length; i += 2)
params.add(new Pair<String, Object>(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<String, Object>(getParameters[i].toString() + "[]",
list.get(j)));
} else
params.add(new Pair<String, Object>(getParameters[i].toString(), getParameters[i+1]));
}
params.add(new Pair<String, Object>("app_id", APP_ID));
params.add(new Pair<String, Object>("time", System.currentTimeMillis() / 1000L));
if(token != null)
params.add(new Pair<String, Object>("token", token));
Collections.sort(params, new Comparator<Pair<String, Object>>() {
@Override
public int compare(Pair<String, Object> object1,
Pair<String, Object> 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();

@ -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;
}
}

@ -1,4 +1,4 @@
package com.todoroo.astrid.sharing;
package com.todoroo.astrid.actfm.sync;
import java.io.IOException;

@ -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<ActFmTaskContainer> {
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<ActFmTaskContainer> remoteTasks = new ArrayList<ActFmTaskContainer>();
serverTime = (int)(fetchRemoteTasks(serverTime, remoteTasks) - DateUtilities.now()/1000L);
fetchRemoteTagData(serverTime);
SyncData<ActFmTaskContainer> 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<ActFmTaskContainer> 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<ActFmTaskContainer> populateSyncData(ArrayList<ActFmTaskContainer> remoteTasks) throws JSONException {
// fetch locally created tasks
TodorooCursor<Task> localCreated = actFmDataService.getLocallyCreated(Task.PROPERTIES);
// fetch locally updated tasks
TodorooCursor<Task> localUpdated = actFmDataService.getLocallyUpdated(Task.PROPERTIES);
return new SyncData<ActFmTaskContainer>(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> metadata = new ArrayList<Metadata>();
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<Task> 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<ActFmTaskContainer> 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));
}
}

@ -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 <tim@todoroo.com>
*
*/
@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<Task>() {
@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<Update>() {
@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<TagData>() {
@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<Object> params = new ArrayList<Object>();
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<Object> params = new ArrayList<Object>();
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<Metadata> 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> metadata = new ArrayList<Metadata>();
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<Object> params = new ArrayList<Object>();
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<Object> array = new ArrayList<Object>(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<TagData>() {
@Override
protected void mergeAndSave(JSONArray list, HashMap<Long,Long> 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<Long, Long> getLocalModels() {
TodorooCursor<TagData> 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<Task>() {
@Override
protected void mergeAndSave(JSONArray list, HashMap<Long,Long> locals) throws JSONException {
Task remote = new Task();
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
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<Long, Long> getLocalModels() {
TodorooCursor<Task> 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<Update>() {
@Override
protected void mergeAndSave(JSONArray list, HashMap<Long,Long> 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<Long, Long> getLocalModels() {
TodorooCursor<Update> 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<TYPE extends AbstractModel> {
protected Long[] remoteIds = null;
abstract protected HashMap<Long, Long> getLocalModels();
abstract protected void mergeAndSave(JSONArray list,
HashMap<Long,Long> locals) throws JSONException;
public void process(JSONArray list) throws JSONException {
readRemoteIds(list);
HashMap<Long, Long> 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<Long, Long> 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<Long, Long> cursorToMap(TodorooCursor<TYPE> cursor, DatabaseDao<?> dao,
LongProperty remoteIdProperty, LongProperty localIdProperty) {
try {
HashMap<Long, Long> map = new HashMap<Long, Long>(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> 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);
}
}
}
}

@ -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 <tim@todoroo.com>
*
*/
public class ActFmTaskContainer extends SyncContainer {
public ActFmTaskContainer(Task task, ArrayList<Metadata> metadata) {
this.task = task;
this.metadata = metadata;
}
@SuppressWarnings("nls")
public ActFmTaskContainer(Task task, ArrayList<Metadata> 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;
}
}

@ -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);
}

@ -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$

@ -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);

@ -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<Metadata> 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();

@ -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<GtasksTaskContainer> {
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<GtasksTaskContainer> {
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<GtasksTaskContainer> {
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<GtasksTaskContainer> {
* 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<GtasksTaskContainer> {
} catch (JSONException e) {
throw new GoogleTasksException(e);
}
return pull(remote);
}
// ----------------------------------------------------------------------

@ -90,4 +90,4 @@ public class NoteViewingActivity extends Activity {
});
body.addView(ok);
}
}
}

@ -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);

@ -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));
}
/**

@ -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<ProducteevTaskContainer> {
@ -100,41 +99,9 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
dataService.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();
preferences.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 ApiServiceException) && e instanceof IOException) {
message = context.getString(R.string.producteev_ioerror);
} 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);
}
protected SyncProviderUtilities getUtilities() {
return ProducteevUtilities.INSTANCE;
}
// ----------------------------------------------------------------------
@ -321,7 +288,6 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_ACTIVITY, lastActivityId);
StatisticsService.reportEvent("pdv-sync-finished"); //$NON-NLS-1$
Flags.set(Flags.REFRESH);
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
@ -487,7 +453,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
* have changed.
*/
@Override
protected void push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException {
protected ProducteevTaskContainer push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException {
boolean remerge = false;
long idTask = local.pdvTask.getValue(ProducteevTask.ID);
@ -496,7 +462,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
// if local is marked do not sync, handle accordingly
if(idDashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) {
return;
return local;
}
// fetch remote task for comparison
@ -578,10 +544,9 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
local.task.setValue(Task.NOTES, "");
}
remote = pull(local);
remote.task.setId(local.task.getId());
if(remerge) {
remote = pull(local);
remote.task.setId(local.task.getId());
// transform local into remote
local.task = remote.task;
local.pdvTask.setValue(ProducteevTask.ID, remote.pdvTask.getValue(ProducteevTask.ID));
@ -591,6 +556,8 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
if(remote.pdvTask.containsNonNullValue(ProducteevTask.REPEATING_SETTING))
local.pdvTask.setValue(ProducteevTask.REPEATING_SETTING, remote.pdvTask.getValue(ProducteevTask.REPEATING_SETTING));
}
return remote;
} catch (JSONException e) {
throw new ApiResponseParseException(e);
}

@ -121,7 +121,7 @@ public class Notifications extends BroadcastReceiver {
Task task;
try {
task = taskDao.fetch(id, Task.ID, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE,
Task.DUE_DATE, Task.DELETION_DATE, Task.REMINDER_FLAGS);
Task.DUE_DATE, Task.DELETION_DATE, Task.REMINDER_FLAGS, Task.USER_ID);
if(task == null)
throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$
@ -130,8 +130,8 @@ public class Notifications extends BroadcastReceiver {
return false;
}
// you're done - don't sound, do delete
if(task.isCompleted() || task.isDeleted())
// you're done, or not yours - don't sound, do delete
if(task.isCompleted() || task.isDeleted() || task.getValue(Task.USER_ID) != 0)
return false;
// it's hidden - don't sound, don't delete
@ -178,19 +178,7 @@ public class Notifications extends BroadcastReceiver {
notificationManager = new AndroidNotificationManager(context);
// quiet hours? unless alarm clock
boolean quietHours = false;
int quietHoursStart = Preferences.getIntegerFromString(R.string.p_rmd_quietStart, -1);
int quietHoursEnd = Preferences.getIntegerFromString(R.string.p_rmd_quietEnd, -1);
if(quietHoursStart != -1 && quietHoursEnd != -1 && ringTimes >= 0) {
int hour = new Date().getHours();
if(quietHoursStart <= quietHoursEnd) {
if(hour >= quietHoursStart && hour < quietHoursEnd)
quietHours = true;
} else { // wrap across 24/hour boundary
if(hour >= quietHoursStart || hour < quietHoursEnd)
quietHours = true;
}
}
boolean quietHours = ringTimes < 0 ? false : isQuietHours();
PendingIntent pendingIntent = PendingIntent.getActivity(context,
notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
@ -313,6 +301,25 @@ public class Notifications extends BroadcastReceiver {
}
}
/**
* @return whether we're in quiet hours
*/
public static boolean isQuietHours() {
int quietHoursStart = Preferences.getIntegerFromString(R.string.p_rmd_quietStart, -1);
int quietHoursEnd = Preferences.getIntegerFromString(R.string.p_rmd_quietEnd, -1);
if(quietHoursStart != -1 && quietHoursEnd != -1) {
int hour = new Date().getHours();
if(quietHoursStart <= quietHoursEnd) {
if(hour >= quietHoursStart && hour < quietHoursEnd)
return true;
} else { // wrap across 24/hour boundary
if(hour >= quietHoursStart || hour < quietHoursEnd)
return true;
}
}
return false;
}
/**
* Schedules alarms for a single task
*

@ -50,7 +50,8 @@ public final class ReminderService {
Task.REMINDER_FLAGS,
Task.REMINDER_PERIOD,
Task.REMINDER_LAST,
Task.REMINDER_SNOOZE
Task.REMINDER_SNOOZE,
Task.FLAGS,
};
/** flag for due date reminder */
@ -176,7 +177,7 @@ public final class ReminderService {
}
}
if(task.isCompleted() || task.isDeleted()) {
if(task.isCompleted() || task.isDeleted() || task.getValue(Task.USER_ID) != 0) {
clearAllAlarms(task);
return;
}
@ -193,18 +194,6 @@ public final class ReminderService {
// notifications after due date
long whenOverdue = calculateNextOverdueReminder(task);
/*if(Constants.DEBUG) {
System.err.println("TASK: " + task.getValue(Task.TITLE));
System.err.println("LAST REMINDER: " + new Date(task.getValue(Task.REMINDER_LAST)));
if(task.hasDueDate())
System.err.println("DUEDATE: " + new Date(task.getValue(Task.DUE_DATE)));
System.err.println("WHEN OVERDUE: " + (whenOverdue));
System.err.println("WHEN DUED: " + (whenDueDate));
System.err.println("WHEN SNOOZ: " + (whenSnooze));
System.err.println("WHEN RANDO: " + (whenRandom));
}*/
// if random reminders are too close to due date, favor due date
if(whenRandom != NO_ALARM && whenDueDate - whenRandom < DateUtilities.ONE_DAY)
whenRandom = NO_ALARM;
@ -484,7 +473,7 @@ public final class ReminderService {
private TodorooCursor<Task> getTasksWithReminders(Property<?>... properties) {
return taskDao.query(Query.select(properties).where(Criterion.and(
TaskCriteria.isActive(),
Criterion.not(TaskCriteria.isReadOnly()),
TaskCriteria.ownedByMe(),
Criterion.or(Task.REMINDER_FLAGS.gt(0), Task.REMINDER_PERIOD.gt(0)))));
}

@ -33,10 +33,14 @@ public class RepeatTaskCompleteListener extends BroadcastReceiver {
return;
Task task = PluginServices.getTaskService().fetchById(taskId, Task.ID, Task.RECURRENCE,
Task.DUE_DATE, Task.FLAGS, Task.HIDE_UNTIL);
Task.DUE_DATE, Task.FLAGS, Task.HIDE_UNTIL, Task.REMOTE_ID);
if(task == null)
return;
// don't repeat when it repeats on the server
if(task.getValue(Task.REMOTE_ID) > 0)
return;
String recurrence = task.getValue(Task.RECURRENCE);
if(recurrence != null && recurrence.length() > 0) {
long newDueDate;

@ -1,67 +0,0 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.sharing;
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.api.AstridApiConstants;
import com.todoroo.astrid.api.TaskAction;
import com.todoroo.astrid.api.TaskDecoration;
/**
* Exposes {@link TaskDecoration} for timers
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class SharingActionExposer extends BroadcastReceiver {
static final String EXTRA_TASK = "task"; //$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())) {
sendAction(context, taskId);
} else {
performAction(context, taskId);
}
}
private void performAction(Context context, long taskId) {
Intent intent = new Intent(context, SharingLoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
private void sendAction(Context context, long taskId) {
final String label = context.getString(R.string.sharing_action);
final Drawable drawable = context.getResources().getDrawable(R.drawable.tango_share);
Bitmap icon = ((BitmapDrawable)drawable).getBitmap();
Intent newIntent = new Intent(context, getClass());
newIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
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_RESPONSE, action);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -1,39 +0,0 @@
package com.todoroo.astrid.sharing;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.astrid.data.Metadata;
/**
* Metadata entry for a task alarm
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class SharingFields {
/** metadata key */
public static final String METADATA_KEY = "sharing"; //$NON-NLS-1$
/** online url */
public static final StringProperty URL = Metadata.VALUE1;
/** sharing privacy */
public static final IntegerProperty PRIVACY = new IntegerProperty(Metadata.TABLE,
Metadata.VALUE2.name);
// --- constants
/** this task is shared publicly */
public static final int PRIVACY_PUBLIC = 2;
/** this task is shared with a limited group */
public static final int PRIVACY_LIMITED = 2;
/** this task is private */
public static final int PRIVACY_PRIVATE = 1;
/** this alarm repeats itself until turned off */
public static final int TYPE_REPEATING = 2;
}

@ -1,124 +0,0 @@
/*
* 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.sharing;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import com.facebook.android.AuthListener;
import com.facebook.android.Facebook;
import com.facebook.android.LoginButton;
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.astrid.data.Task;
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 SharingLoginActivity extends Activity implements AuthListener {
public static final String APP_ID = "169904866369148"; //$NON-NLS-1$
@Autowired TaskService taskService;
private Facebook facebook;
private TextView errors;
// --- ui initialization
static {
AstridDependencyInjector.initialize();
}
public String EXTRA_TASK_ID = "task"; //$NON-NLS-1$
public SharingLoginActivity() {
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);
long taskId = getIntent().getLongExtra(EXTRA_TASK_ID, 4L);
Task task = taskService.fetchById(taskId, Task.TITLE);
TextView taskInfo = (TextView) findViewById(R.id.taskInfo);
taskInfo.setText(taskInfo.getText() + "\n\n" + task.getValue(Task.TITLE));
facebook = new Facebook(APP_ID);
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"
});
}
// --- facebook handler
public void onFBAuthSucceed() {
System.err.println("GOTCHA SUCCESS! " + facebook.getAccessToken());
errors.setVisibility(View.VISIBLE);
}
public void onFBAuthFail(String error) {
System.err.println("GOTCHA ERROR: " + error);
DialogUtilities.okDialog(this, getString(R.string.sharing_SLA_title),
android.R.drawable.ic_dialog_alert, error, null);
}
@Override
public void onFBAuthCancel() {
System.err.println("GOTCHA CANCEL");
// do nothing
}
// --- my astrid handler
/**
* Create user account via FB
*/
public void createUserAccountFB() {
String accessToken = facebook.getAccessToken();
}
}

@ -92,7 +92,7 @@ public class FilterByTagExposer extends BroadcastReceiver {
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Tag tag = new Tag(tags.get(which), 0);
Tag tag = new Tag(tags.get(which), 0, 0);
String listTitle = tag.tag;
String title = ContextManager.getString(

@ -3,12 +3,12 @@
*/
package com.todoroo.astrid.tags;
import java.util.Comparator;
import java.util.HashMap;
import java.util.TreeSet;
import java.util.ArrayList;
import java.util.HashSet;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
@ -21,19 +21,28 @@ import android.widget.EditText;
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.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.actfm.TagViewActivity;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterCategory;
import com.todoroo.astrid.api.FilterListHeader;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.api.FilterWithCustomIntent;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.tags.TagService.Tag;
/**
@ -46,17 +55,23 @@ public class TagFilterExposer extends BroadcastReceiver {
private static final String TAG = "tag"; //$NON-NLS-1$
@Autowired TagDataService tagDataService;
@Autowired ActFmPreferenceService actFmPreferenceService;
private TagService tagService;
private Filter filterFromTag(Context context, Tag tag, Criterion criterion) {
String listTitle = tag.tag;
/** Create filter from new tag object */
@SuppressWarnings("nls")
public static Filter filterFromTag(Context context, Tag tag, Criterion criterion, boolean useTagViewActivity) {
String listTitle = tag.tag + " (" + tag.count + ")";
String title = context.getString(R.string.tag_FEx_name, tag.tag);
QueryTemplate tagTemplate = tag.queryTemplate(criterion);
ContentValues contentValues = new ContentValues();
contentValues.put(Metadata.KEY.name, TagService.KEY);
contentValues.put(TagService.TAG.name, tag.tag);
Filter filter = new Filter(listTitle,
FilterWithCustomIntent filter = new FilterWithCustomIntent(listTitle,
title, tagTemplate,
contentValues);
if(tag.count == 0)
@ -70,11 +85,28 @@ public class TagFilterExposer extends BroadcastReceiver {
newTagIntent(context, RenameTagActivity.class, tag),
newTagIntent(context, DeleteTagActivity.class, tag)
};
if(useTagViewActivity) {
filter.customTaskList = new ComponentName(ContextManager.getContext(), TagViewActivity.class);
Bundle extras = new Bundle();
extras.putString(TagViewActivity.EXTRA_TAG_NAME, tag.tag);
extras.putLong(TagViewActivity.EXTRA_TAG_REMOTE_ID, tag.remoteId);
filter.customExtras = extras;
} else {
filter.customTaskList = new ComponentName(ContextManager.getContext(), TaskListActivity.class);
}
return filter;
}
private Intent newTagIntent(Context context, Class<? extends Activity> activity, Tag tag) {
/** Create a filter from tag data object */
public static Filter filterFromTagData(Context context, TagData tagData) {
Tag tag = new Tag(tagData.getValue(TagData.NAME),
tagData.getValue(TagData.TASK_COUNT),
tagData.getValue(TagData.REMOTE_ID));
return filterFromTag(context, tag, TaskCriteria.activeAndVisible(), true);
}
private static Intent newTagIntent(Context context, Class<? extends Activity> activity, Tag tag) {
Intent ret = new Intent(context, activity);
ret.putExtra(TAG, tag.tag);
return ret;
@ -82,81 +114,108 @@ public class TagFilterExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
DependencyInjectionService.getInstance().inject(this);
ContextManager.setContext(context);
tagService = TagService.getInstance();
Tag[] tags = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE, TaskCriteria.notDeleted());
// If user does not have any tags, don't show this section at all
if(tags.length == 0)
return;
// sort tags by # of active tasks
Tag[] activeTags = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE, TaskCriteria.activeAndVisible());
HashMap<String, Integer> actives = new HashMap<String, Integer>();
for(Tag tag : activeTags)
actives.put(tag.tag, tag.count);
TreeSet<Tag> sortedTagSet = new TreeSet<Tag>(new Comparator<Tag>() {
@Override
public int compare(Tag a, Tag b) {
if(a.count == b.count)
return a.tag.compareTo(b.tag);
return b.count - a.count;
}
});
for(Tag tag : tags) {
if(!actives.containsKey(tag.tag))
tag.count = 0;
else {
// will decrease tag.count is there are tasks with this tag which are not activeAndVisible but also have not been deleted
tag.count = actives.get(tag.tag);
}
sortedTagSet.add(tag);
}
// create filter list
Resources r = context.getResources();
FilterListItem[] list = new FilterListItem[3];
ArrayList<FilterListItem> list = new ArrayList<FilterListItem>();
// --- header
FilterListHeader tagsHeader = new FilterListHeader(context.getString(R.string.tag_FEx_header));
list[0] = tagsHeader;
list.add(tagsHeader);
// --- untagged
Filter untagged = new Filter(r.getString(R.string.tag_FEx_untagged),
r.getString(R.string.tag_FEx_untagged),
tagService.untaggedTemplate(),
null);
untagged.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_untagged)).getBitmap();
list[1] = untagged;
list.add(untagged);
Filter[] filters = new Filter[sortedTagSet.size()];
int index = 0;
for(Tag tag : sortedTagSet) {
filters[index++] = filterFromTag(context, tag,
Criterion.and(TaskCriteria.activeAndVisible(), Criterion.not(TaskCriteria.isReadOnly())));
}
FilterCategory tagsFilter = new FilterCategory(context.getString(R.string.tag_FEx_by_size), filters);
list[2] = tagsFilter;
addTags(list);
// transmit filter list
if(list.size() <= 2)
return;
FilterListItem[] listAsArray = list.toArray(new FilterListItem[list.size()]);
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, list);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, listAsArray);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, TagsPlugin.IDENTIFIER);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
private void addTags(ArrayList<FilterListItem> list) {
HashSet<String> tagNames = new HashSet<String>();
// active tags
Tag[] myTags = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE,
Criterion.and(TaskCriteria.ownedByMe(), TaskCriteria.activeAndVisible()));
for(Tag tag : myTags)
tagNames.add(tag.tag);
if(myTags.length > 0)
list.add(filterFromTags(myTags, R.string.tag_FEx_category_mine));
// find all tag data not in active tag list
TodorooCursor<TagData> cursor = tagDataService.query(Query.select(
TagData.NAME, TagData.TASK_COUNT, TagData.REMOTE_ID));
ArrayList<Tag> notListed = new ArrayList<Tag>();
try {
ArrayList<Tag> sharedTags = new ArrayList<Tag>();
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
String tagName = cursor.get(TagData.NAME);
if(tagNames.contains(tagName))
continue;
Tag tag = new Tag(tagName, cursor.get(TagData.TASK_COUNT),
cursor.get(TagData.REMOTE_ID));
if(tag.count > 0)
sharedTags.add(tag);
else
notListed.add(tag);
tagNames.add(tagName);
}
if(sharedTags.size() > 0)
list.add(filterFromTags(sharedTags.toArray(new Tag[sharedTags.size()]), R.string.tag_FEx_category_shared));
} finally {
cursor.close();
}
// find inactive tags, intersect tag list
Tag[] inactiveTags = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_ALPHA,
Criterion.and(TaskCriteria.notDeleted(), Criterion.not(TaskCriteria.activeAndVisible())));
for(Tag tag : inactiveTags) {
if(!tagNames.contains(tag.tag)) {
notListed.add(tag);
tag.count = 0;
}
}
if(notListed.size() > 0)
list.add(filterFromTags(notListed.toArray(new Tag[notListed.size()]),
R.string.tag_FEx_category_inactive));
}
private FilterCategory filterFromTags(Tag[] tags, int name) {
Filter[] filters = new Filter[tags.length];
Context context = ContextManager.getContext();
for(int i = 0; i < tags.length; i++)
filters[i] = filterFromTag(context, tags[i], TaskCriteria.activeAndVisible(),
actFmPreferenceService.isLoggedIn());
return new FilterCategory(context.getString(name), filters);
}
// --- tag manipulation activities
public abstract static class TagActivity extends Activity {
protected String tag;
@Autowired public TagService tagService;
@Autowired public TagDataService tagDataService;
static {
AstridDependencyInjector.initialize();
}
protected TagActivity() {
DependencyInjectionService.getInstance().inject(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -167,9 +226,14 @@ public class TagFilterExposer extends BroadcastReceiver {
finish();
return;
}
DependencyInjectionService.getInstance().inject(this);
DependencyInjectionService.getInstance().inject(this); // why?
TagData tagData = tagDataService.getTag(tag, TagData.MEMBER_COUNT);
if(tagData != null && tagData.getValue(TagData.MEMBER_COUNT) > 0) {
DialogUtilities.okDialog(this, getString(R.string.actfm_tag_operation_disabled), getCancelListener());
return;
}
showDialog();
}
@ -239,12 +303,15 @@ public class TagFilterExposer extends BroadcastReceiver {
@Override
protected void showDialog() {
editor = new EditText(this); // not sure why this can't be done in the RenameTagActivity constructor.
editor = new EditText(this);
DialogUtilities.viewDialog(this, getString(R.string.DLG_rename_this_tag_header, tag), editor, getOkListener(), getCancelListener());
}
@Override
protected boolean ok() { // this interface is not going to work well with the dialog that says "Are you sure?"
protected boolean ok() {
if(editor == null)
return false;
String text = editor.getText().toString();
if (text == null || text.length() == 0) {
return false;

@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.LinkedHashSet;
import com.todoroo.andlib.data.Property.CountProperty;
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;
@ -41,6 +42,9 @@ public final class TagService {
/** Property for reading tag values */
public static final StringProperty TAG = Metadata.VALUE1;
/** Property for astrid.com remote id */
public static final LongProperty REMOTE_ID = new LongProperty(Metadata.TABLE, Metadata.VALUE2.name);
// --- singleton
private static TagService instance = null;
@ -79,10 +83,12 @@ public final class TagService {
public static final class Tag {
public String tag;
int count;
long remoteId;
public Tag(String tag, int count) {
public Tag(String tag, int count, long remoteId) {
this.tag = tag;
this.count = count;
this.remoteId = remoteId;
}
@Override
@ -112,7 +118,7 @@ public final class TagService {
return new QueryTemplate().where(Criterion.and(
Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).where(MetadataCriteria.withKey(KEY)))),
TaskCriteria.isActive(),
Criterion.not(TaskCriteria.isReadOnly()),
TaskCriteria.ownedByMe(),
TaskCriteria.isVisible()));
}
@ -124,7 +130,7 @@ public final class TagService {
* @return empty array if no tags, otherwise array
*/
public Tag[] getGroupedTags(Order order, Criterion activeStatus) {
Query query = Query.select(TAG.as(TAG.name), COUNT).
Query query = Query.select(TAG, REMOTE_ID, COUNT).
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
where(Criterion.and(activeStatus, MetadataCriteria.withKey(KEY))).
orderBy(order).groupBy(TAG);
@ -133,7 +139,7 @@ public final class TagService {
Tag[] array = new Tag[cursor.getCount()];
for (int i = 0; i < array.length; i++) {
cursor.moveToNext();
array[i] = new Tag(cursor.get(TAG), cursor.get(COUNT));
array[i] = new Tag(cursor.get(TAG), cursor.get(COUNT), cursor.get(REMOTE_ID));
}
return array;
} finally {
@ -148,7 +154,7 @@ public final class TagService {
* @return cursor. PLEASE CLOSE THE CURSOR!
*/
public TodorooCursor<Metadata> getTags(long taskId) {
Query query = Query.select(TAG).where(Criterion.and(MetadataCriteria.withKey(KEY),
Query query = Query.select(TAG, REMOTE_ID).where(Criterion.and(MetadataCriteria.withKey(KEY),
MetadataCriteria.byTask(taskId)));
return metadataDao.query(query);
}

@ -30,6 +30,7 @@ import com.todoroo.astrid.activity.TaskEditActivity.TaskEditControlSet;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.tags.TagService.Tag;
import com.todoroo.astrid.utility.Flags;
/**
* Control set to manage adding and removing tags
@ -57,7 +58,7 @@ public final class TagsControlSet implements TaskEditControlSet {
tagSpinner.setVisibility(View.GONE);
} else {
ArrayList<Tag> dropDownList = new ArrayList<Tag>(Arrays.asList(allTags));
dropDownList.add(0, new Tag(activity.getString(R.string.TEA_tag_dropdown), 0));
dropDownList.add(0, new Tag(activity.getString(R.string.TEA_tag_dropdown), 0, 0));
ArrayAdapter<Tag> tagAdapter = new ArrayAdapter<Tag>(activity,
android.R.layout.simple_spinner_item,
dropDownList);
@ -116,8 +117,10 @@ public final class TagsControlSet implements TaskEditControlSet {
tags.add(tagName.getText().toString());
}
if(TagService.getInstance().synchronizeTags(task.getId(), tags))
if(TagService.getInstance().synchronizeTags(task.getId(), tags)) {
Flags.set(Flags.TAGS_CHANGED);
task.setValue(Task.MODIFICATION_DATE, DateUtilities.now());
}
return null;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/sharing_button_normal" />
<item android:state_pressed="true" android:state_focused="false" android:drawable="@drawable/sharing_button_pressed" />
<item android:state_pressed="false" android:state_focused="true" android:drawable="@drawable/sharing_button_focused" />
<item android:state_pressed="true" android:state_focused="true" android:drawable="@drawable/sharing_button_pressed" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="radial"
android:startColor="#0663be"
android:endColor="#003471"
android:gradientRadius="300"
android:centerX="0.5"
android:centerY="0.5" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:drawable/list_selector_background"
android:padding="4dip"
android:paddingRight="6dip"
android:orientation="horizontal">
<!-- picture thumbnail -->
<ImageView android:id="@android:id/icon"
android:layout_width="40dip"
android:layout_height="40dip"
android:gravity="center"
android:layout_marginRight="5dip"
android:scaleType="fitCenter" />
<!-- person or tag name -->
<TextView android:id="@android:id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
style="@android:attr/dropDownItemStyle"
android:textAppearance="@android:attr/textAppearanceLargeInverse"
android:textSize="20sp"/>
</LinearLayout>

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<com.todoroo.astrid.ui.ContactsAutoComplete
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="@string/actfm_person_hint" />
<ImageButton android:id="@+id/button1"
style="?android:attr/buttonStyleInset"
android:src="@android:drawable/ic_delete"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginTop="2dip"
android:layout_marginRight="2dip"
android:layout_marginBottom="2dip"
android:gravity="center_vertical"
/>
</LinearLayout>

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:astrid="http://schemas.android.com/apk/res/com.timsu.astrid"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@drawable/background_gradient">
<ScrollView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="100">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dip"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
style="@style/TextAppearance.TAd_ItemTitle"
android:paddingBottom="15dip"
android:paddingTop="10dip"
android:textSize="22sp" />
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="@android:drawable/divider_horizontal_dark" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="15dip"
android:paddingBottom="5dip"
android:text="@string/actfm_EPA_assign_label" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Spinner
android:id="@+id/assigned_spinner"
android:layout_width="fill_parent"
android:layout_height="45dip"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="@string/actfm_person_hint" />
<com.todoroo.astrid.ui.ContactsAutoComplete
android:id="@+id/assigned_custom"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:visibility="gone"
android:hint="@string/actfm_person_hint" />
<ImageButton android:id="@+id/assigned_clear"
style="?android:attr/buttonStyleInset"
android:src="@android:drawable/ic_delete"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginTop="2dip"
android:layout_marginRight="2dip"
android:layout_marginBottom="2dip"
android:visibility="gone"
android:gravity="center_vertical" />
</LinearLayout>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="5dip"
android:text="@string/actfm_EPA_share_with" />
<com.todoroo.astrid.ui.PeopleContainer
android:id="@+id/share_container"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
astrid:completeTags="true" />
<LinearLayout
android:id="@+id/share_additional"
android:orientation="vertical"
android:padding="5dip"
android:visibility="gone"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="@android:drawable/divider_horizontal_dark" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:paddingBottom="5dip"
android:text="@string/actfm_EPA_message_text" />
<EditText
android:id="@+id/message"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:text="@string/actfm_EPA_message_body"
android:autoText="true"
android:capitalize="sentences"
android:singleLine="false" />
<TextView
android:id="@+id/tag_label"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="5dip"
android:visibility="gone"
android:text="@string/actfm_EPA_tag_label" />
<EditText
android:id="@+id/tag_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:hint="@string/actfm_EPA_tag_hint" />
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="@android:drawable/divider_horizontal_dark" />
</LinearLayout>
<CheckBox
android:id="@+id/checkbox_facebook"
android:text="@string/actfm_EPA_facebook"
android:paddingLeft="45dip"
android:visibility="gone"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<CheckBox
android:id="@+id/checkbox_twitter"
android:text="@string/actfm_EPA_twitter"
android:paddingLeft="45dip"
android:visibility="gone"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginTop="10dip"
android:padding="5dip"
android:orientation="horizontal"
android:background="@drawable/edit_header"
android:baselineAligned="false">
<ImageButton
android:id="@+id/save"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/tango_save" />
<ImageButton
android:id="@+id/discard"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/tango_stop" />
</LinearLayout>
</LinearLayout>

@ -1,95 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffff">
<LinearLayout
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="5px"
android:paddingRight="5px"
android:orientation="vertical">
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="3dip"
android:paddingBottom="3dip"
android:background="#2d1110"
android:scaleType="fitCenter"
android:src="@drawable/sharing_logo" />
android:layout_height="fill_parent"
android:paddingLeft="4px"
android:paddingRight="4px"
android:background="@drawable/sharing_gradient">
<TextView
android:id="@+id/intro"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="5dip"
android:paddingBottom="10dip"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:textSize="16sp"
android:textColor="#333333"
android:paddingTop="20dip"
android:textColor="#ffffff"
android:text="@string/sharing_SLA_body" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="20dip"
android:textSize="20sp"
android:textColor="#333333"
android:text="@string/sharing_SLA_login"
android:textStyle="bold" />
<TextView
android:id="@+id/error"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:paddingBottom="20dip"
android:layout_below="@id/intro"
android:layout_alignParentLeft="true"
android:gravity="center"
android:paddingTop="10dip"
android:textColor="#ff0000"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
android:textStyle="bold" />
<LinearLayout
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.facebook.android.LoginButton
android:id="@+id/fb_login"
android:src="@drawable/facebook_64"
android:background="#00000000"
android:gravity="right"
android:paddingLeft="10px"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageButton
android:id="@+id/google_login"
android:src="@drawable/google_64"
android:background="#00000000"
android:gravity="left"
android:paddingRight="10px"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:scaleType="fitCenter"
android:paddingLeft="20dip"
android:paddingRight="20dip"
android:src="@drawable/sharing_logo" />
<TextView
android:id="@+id/notice"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:gravity="center"
android:paddingTop="10dip"
android:paddingBottom="20dip"
android:paddingTop="30dip"
android:paddingBottom="30dip"
android:textSize="14sp"
android:textColor="#333333"
android:text="@string/sharing_SLA_next_step" />
<TextView
android:id="@+id/taskInfo"
android:textColor="#cccccc"
android:text="@string/sharing_SLA_notice" />
<com.facebook.android.LoginButton
android:id="@+id/fb_login"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:textColor="#333333"
android:text="@string/sharing_SLA_task"
android:textStyle="bold" />
android:layout_height="50dip"
android:layout_above="@id/notice"
android:layout_alignParentLeft="true"
android:paddingLeft="20dip"
android:paddingRight="20dip"
android:textSize="18sp"
android:text="@string/sharing_SLA_login" />
</LinearLayout>
</ScrollView>
</RelativeLayout>

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:astrid="http://schemas.android.com/apk/res/com.timsu.astrid"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:drawable/list_selector_background"
@ -57,6 +58,16 @@
android:visibility="gone" />
</LinearLayout>
<!-- assignee photo -->
<greendroid.widget.AsyncImageView android:id="@+id/picture"
android:layout_width="35dip"
android:layout_height="fill_parent"
android:padding="5dip"
android:scaleType="fitCenter"
astrid:defaultSrc="@drawable/ic_contact_picture_2"
android:visibility="gone"/>
</LinearLayout>
<TextView android:id="@+id/extendedDetails"

@ -3,7 +3,7 @@
<android.gesture.GestureOverlayView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gestures"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="100"
android:gestureColor="#00000000"

@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:astrid="http://schemas.android.com/apk/res/com.timsu.astrid"
android:id="@android:id/tabhost"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="100">
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TabWidget
android:id="@android:id/tabs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-2dp"
android:layout_marginRight="-2dp"
android:layout_weight="1"
android:background="@null" />
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="100">
<!-- task list body automatically inserted -->
<!-- updates tab -->
<ListView
android:id="@+id/tab_updates"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<!-- settings tab -->
<ScrollView
android:id="@+id/tab_settings"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="5dip"
android:orientation="vertical">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dip">
<greendroid.widget.AsyncImageView
android:id="@+id/picture"
android:layout_width="80dip"
android:layout_height="80dip"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:scaleType="fitCenter"
android:paddingRight="10dip"
astrid:defaultSrc="@android:drawable/ic_menu_gallery" />
<TextView
android:id="@+id/tag_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/picture"
android:layout_alignParentTop="true"
android:layout_marginTop="10dip"
android:text="@string/actfm_TVA_tag_label" />
<EditText
android:id="@+id/tag_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/picture"
android:layout_below="@id/tag_label"
android:layout_marginTop="10dip" />
</RelativeLayout>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:text="@string/actfm_TVA_tag_owner_label" />
<TextView
android:id="@+id/tag_owner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dip"
android:textSize="20sp"/>
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="@android:drawable/divider_horizontal_dark" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="10dip"
android:paddingBottom="5dip"
android:text="@string/actfm_TVA_members_label" />
<com.todoroo.astrid.ui.PeopleContainer
android:id="@+id/members_container"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
</FrameLayout>
<!-- Footer -->
<LinearLayout
android:id="@+id/updatesFooter"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:orientation="horizontal">
<!-- Add Button -->
<ImageButton android:id="@+id/commentButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:src="@drawable/tango_chat"
android:scaleType="fitCenter"/>
<!-- Comment Field -->
<EditText android:id="@+id/commentField"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="100"
android:hint="@string/TVA_add_comment"
android:singleLine="true"
android:autoText="true"
android:capitalize="sentences"/>
</LinearLayout>
<LinearLayout
android:id="@+id/membersFooter"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:layout_marginTop="10dip"
android:padding="5dip"
android:background="@drawable/edit_header"
android:orientation="horizontal"
android:baselineAligned="false">
<ImageButton
android:id="@+id/saveMembers"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/tango_save" />
</LinearLayout>
</LinearLayout>
</TabHost>

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:astrid="http://schemas.android.com/apk/res/com.timsu.astrid"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:drawable/list_selector_background"
android:paddingTop="4dip"
android:paddingBottom="4dip"
android:paddingLeft="4dip"
android:paddingRight="6dip">
<!-- picture -->
<greendroid.widget.AsyncImageView android:id="@+id/picture"
android:layout_width="40dip"
android:layout_height="40dip"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:paddingTop="5dip"
astrid:defaultSrc="@drawable/ic_contact_picture_2"
android:scaleType="fitCenter" />
<!-- title -->
<TextView android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:paddingLeft="50dip"
android:paddingRight="75dip"
style="@style/TextAppearance.TAd_ItemTitle"
android:textSize="16sp"/>
<!-- action description -->
<TextView android:id="@+id/description"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_alignParentLeft="true"
android:paddingLeft="50dip"
android:textSize="14sp" />
<!-- activity date -->
<TextView android:id="@+id/date"
android:layout_width="75dip"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:paddingTop="3dip"
style="@style/TextAppearance.TAd_ItemDueDate"
android:gravity="right"
android:ellipsize="end"
android:textSize="12sp"
android:singleLine="true"/>
</RelativeLayout>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<declare-styleable name="ContactsAutoComplete">
<attr name="allowMultiple" format="boolean"/>
<attr name="completeTags" format="boolean"/>
</declare-styleable>
</resources>

@ -267,6 +267,10 @@
<!-- =========================================================== GTASKS == -->
<string name="gtasks_GPr_interval_key">gtasks_sync_freq</string>
<!-- ========================================================== SHARING == -->
<string name="actfm_APr_interval_key">actfm_sync_freq</string>
<!-- ======================================================== POWERPACK == -->

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ================================================== general terms == -->
<!-- People Editing Activity -->
<string name="EPE_action">Share</string>
<!-- task sharing dialog: assigned hint -->
<string name="actfm_person_hint">Contact Name</string>
<!-- task sharing dialog: shared with hint -->
<string name="actfm_person_or_tag_hint">Contact or Shared Tag</string>
<!-- toast on transmit success -->
<string name="actfm_toast_success">Saved on Server</string>
<!-- toast on transmit error -->
<string name="actfm_toast_error">Save Unsuccessful</string>
<!-- can't rename or delete shared tag message -->
<string name="actfm_tag_operation_disabled">Please view shared tags to rename or delete them.</string>
<!-- menu item to take a picture -->
<string name="actfm_picture_camera">Take a Picture</string>
<!-- menu item to select from gallery -->
<string name="actfm_picture_gallery">Pick from Gallery</string>
<!-- filter list activity: refresh tags -->
<string name="actfm_FLA_menu_refresh">Refresh Tags</string>
<!-- =============================================== ProjectViewActivity == -->
<!-- Tag View Activity: Add Comment hint -->
<string name="TVA_add_comment">Add a comment...</string>
<!-- Tag View Activity: task comment ($1 - user name, $2 - task title) -->
<string name="UAd_title_comment">%1$s re: %2$s</string>
<!-- Tabs for Tag view -->
<string-array name="TVA_tabs">
<!-- Tab for showing tasks -->
<item>Tasks</item>
<!-- Tab for showing comments & updates -->
<item>Activity</item>
<!-- Tab for showing setting -->
<item>Members</item>
</string-array>
<!-- Tag View Menu: refresh -->
<string name="actfm_TVA_menu_refresh">Refresh</string>
<!-- Tag Members: tag name label -->
<string name="actfm_TVA_tag_label">Tag Name:</string>
<!-- Tag Members: tag owner label -->
<string name="actfm_TVA_tag_owner_label">Tag Owner:</string>
<!-- Tag Members: tag owner value when there is no owner -->
<string name="actfm_TVA_tag_owner_none">none</string>
<!-- Tag Members: team members label -->
<string name="actfm_TVA_members_label">Team Members:</string>
<!-- Tag Members: tag picture -->
<string name="actfm_TVA_tag_picture">Tag Picture</string>
<!-- ============================================ edit people dialog == -->
<!-- task sharing dialog: window title -->
<string name="actfm_EPA_title">Share / Assign</string>
<!-- task sharing dialog: assigned label -->
<string name="actfm_EPA_assign_label">Assigned to:</string>
<!-- task sharing dialog: assigned to me -->
<string name="actfm_EPA_assign_me">Me</string>
<!-- task sharing dialog: custom email assignment -->
<string name="actfm_EPA_assign_custom">Custom...</string>
<!-- task sharing dialog: shared with label -->
<string name="actfm_EPA_share_with">Shared With:</string>
<!-- task sharing dialog: assigned hint -->
<string name="actfm_EPA_assigned_hint">Contact Name</string>
<!-- task sharing dialog: message label text -->
<string name="actfm_EPA_message_text">Invitation Message:</string>
<!-- task sharing dialog: message body -->
<string name="actfm_EPA_message_body">Help me get this done!</string>
<!-- task sharing dialog: message hint -->
<string name="actfm_EPA_tag_label">Create a shared tag?</string>
<!-- task sharing dialog: message hint -->
<string name="actfm_EPA_tag_hint">(i.e. Silly Hats Club)</string>
<!-- task sharing dialog: share with Facebook -->
<string name="actfm_EPA_facebook">Facebook</string>
<!-- task sharing dialog: share with Twitter -->
<string name="actfm_EPA_twitter">Twitter</string>
<!-- task sharing dialog: # of e-mails sent (%s => # people plural string) -->
<string name="actfm_EPA_emailed_toast">Task shared with %s</string>
<!-- task sharing dialog: edit people settings saved -->
<string name="actfm_EPA_saved_toast">People Settings Saved</string>
<!-- task sharing dialog: invalid email (%s => email) -->
<string name="actfm_EPA_invalid_email">Invalid E-mail: %s</string>
<!-- task sharing dialog: tag not found (%s => tag) -->
<string name="actfm_EPA_invalid_tag">Tag Not Found: %s</string>
<!-- ========================================= sharing login activity == -->
<!-- share login: Title -->
<string name="sharing_SLA_title">Welcome to Astrid.com!</string>
<!-- share login: Sharing Description -->
<string name="sharing_SLA_body">Astrid.com lets you access your tasks online,
and share &amp; delegate with others. Perfect for personal use, friends &amp; family,
or your work groups!</string>
<!-- share login: Sharing Login Prompt -->
<string name="sharing_SLA_login">Sign in using Facebook</string>
<!-- share login: Sharing notice -->
<string name="sharing_SLA_notice">We won\'t post messages or send
e-mails without your permission.</string>
<!-- ================================================ Synchronization == -->
<!-- Preferences Title: Act.fm -->
<string name="actfm_APr_header">Astrid.com (Beta!)</string>
<!-- title for notification tray after synchronizing -->
<string name="actfm_notification_title">Astrid.com Sync</string>
<!-- text for notification when comments are received -->
<string name="actfm_notification_comments">New comments received / click for more details</string>
</resources>

@ -49,6 +49,9 @@
<!-- EULA title -->
<string name="DLG_eula_title">Astrid Terms Of Use</string>
<!-- Progress Dialog generic text -->
<string name="DLG_please_wait">Please Wait</string>
<!-- =============================================================== UI == -->
@ -61,6 +64,15 @@
<!-- String formatter for Disable button -->
<string name="WID_disableButton">Disable</string>
<!-- ============================================================= notes -->
<!-- Note screen: quick-add comment text -->
<string name="notes_quick_add_comment_hint">Add a comment...</string>
<!-- Note screen: blank notes -->
<string name="notes_blank">No notes!</string>
<!-- ================================================= TaskListActivity == -->
<!-- Task List: Displayed instead of list when no items present -->

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ============================================================= UI == -->
<!-- task action: Share -->
<string name="sharing_action">Share</string>
<!-- share login: Title -->
<string name="sharing_SLA_title">Share This Task</string>
<!-- share login: Sharing Description -->
<string name="sharing_SLA_body">My Astrid lets you post tasks to the web to share with
others. Let your friends encourage and keep you accountable!</string>
<!-- share login: Sharing Login Prompt -->
<string name="sharing_SLA_login">Sign in using your Facebook or Google account:</string>
<!-- share login: Next Step information -->
<string name="sharing_SLA_next_step">On the next page,
you can choose how to share this task and choose recipients.</string>
<!-- share login: Sharing Task Information -->
<string name="sharing_SLA_task">Task to Share:</string>
</resources>

@ -25,8 +25,14 @@
<!-- filter header for tags -->
<string name="tag_FEx_header">Tags</string>
<!-- filter header for tags, sorted by size -->
<string name="tag_FEx_by_size">Sorted By Size</string>
<!-- filter header for tags user created -->
<string name="tag_FEx_category_mine">My Tags</string>
<!-- filter header for tags, shared with user -->
<string name="tag_FEx_category_shared">Shared With Me</string>
<!-- filter header for tags which have no active tasks -->
<string name="tag_FEx_category_inactive">Inactive</string>
<!-- filter for untagged tasks -->
<string name="tag_FEx_untagged">Untagged</string>

@ -22,19 +22,16 @@
<!--================================================== Custom Dialogs == -->
<style name="DialogTitleText" parent="android:TextAppearance.WindowTitle">
<item name="android:textSize">20dip</item>
</style>
<style name="DialogTitle" parent="android:WindowTitle">
<item name="android:textSize">18sp</item>
<item name="android:background">#323331</item>
<item name="android:layout_height">26dip</item>
<item name="android:textAppearance">@style/DialogTitleText</item>
</style>
<style name="Theme.Dialog" parent="android:Theme.Dialog">
<item name="android:layout_width">fill_parent</item>
<item name="android:windowTitleSize">18dip</item>
<item name="android:windowTitleStyle">@style/DialogTitle</item>
<item name="android:windowTitleBackgroundStyle">@style/DialogTitle</item>
</style>
<!--=============================================== TaskListActivity == -->

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/sync_SPr_group_status">
<Preference
android:layout="@layout/status_preference"
android:key="@string/sync_SPr_status_key"
android:textSize="24sp"
android:gravity="center"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/sync_SPr_group_options">
<ListPreference
android:key="@string/actfm_APr_interval_key"
android:entries="@array/sync_SPr_interval_entries"
android:entryValues="@array/sync_SPr_interval_values"
android:title="@string/sync_SPr_interval_title" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/sync_SPr_group_actions">
<Preference
android:key="@string/sync_SPr_sync_key"
android:title="@string/sync_SPr_sync" />
<Preference
android:key="@string/sync_SPr_forget_key"
android:title="@string/sync_SPr_forget"
android:summary="@string/sync_SPr_forget_description" />
</PreferenceCategory>
</PreferenceScreen>

@ -40,7 +40,6 @@ import android.content.Intent;
import android.content.res.Resources;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
@ -50,11 +49,11 @@ import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.sync.SyncProvider;
import com.todoroo.astrid.sync.SyncProviderUtilities;
public class MilkSyncProvider extends SyncProvider<MilkTaskContainer> {
@ -84,47 +83,9 @@ public class MilkSyncProvider extends SyncProvider<MilkTaskContainer> {
milkMetadataService.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 showError) {
final Context context = ContextManager.getContext();
MilkUtilities.INSTANCE.setLastError(e.toString());
String message = null;
// occurs when application was closed
if(e instanceof IllegalStateException) {
Log.e(tag, "caught", e); //$NON-NLS-1$
// occurs when network error
} else if(e instanceof ServiceInternalException &&
((ServiceInternalException)e).getEnclosedException() instanceof
IOException) {
Exception enclosedException = ((ServiceInternalException)e).getEnclosedException();
message = context.getString(R.string.rmilk_ioerror);
Log.e(tag, "ioexception", enclosedException); //$NON-NLS-1$
} else {
if(e instanceof ServiceInternalException)
e = ((ServiceInternalException)e).getEnclosedException();
if(e != null)
message = e.toString();
Log.e(tag, "unhandled", e); //$NON-NLS-1$
}
if(showError && context instanceof Activity && message != null) {
DialogUtilities.okDialog((Activity)context, message, null);
}
protected SyncProviderUtilities getUtilities() {
return MilkUtilities.INSTANCE;
}
// ----------------------------------------------------------------------
@ -409,7 +370,7 @@ public class MilkSyncProvider extends SyncProvider<MilkTaskContainer> {
* have changed.
*/
@Override
protected void push(MilkTaskContainer local, MilkTaskContainer remote) throws IOException {
protected MilkTaskContainer push(MilkTaskContainer local, MilkTaskContainer remote) throws IOException {
boolean remerge = false;
// fetch remote task for comparison
@ -492,10 +453,9 @@ public class MilkSyncProvider extends SyncProvider<MilkTaskContainer> {
}
}
remote = pull(local);
remote.task.setId(local.task.getId());
if(remerge) {
remote = pull(local);
remote.task.setId(local.task.getId());
// transform local into remote
local.task = remote.task;
local.listId = remote.listId;
@ -503,6 +463,8 @@ public class MilkSyncProvider extends SyncProvider<MilkTaskContainer> {
local.repeating = remote.repeating;
local.taskSeriesId = remote.taskSeriesId;
}
return remote;
}
/** Create a task container for the given RtmTaskSeries */

@ -42,6 +42,7 @@ import com.todoroo.astrid.helper.MetadataHelper;
import com.todoroo.astrid.service.AddOnService;
import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.ui.ContactListAdapter;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Flags;
import com.todoroo.astrid.voice.VoiceInputAssistant;
@ -210,6 +211,17 @@ public class EditPreferences extends TodorooPreferenceActivity {
}
});
group.addPreference(preference);
preference = new Preference(this);
preference.setTitle("Make lots of contacts");
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference p) {
ContactListAdapter.makeLotsOfContacts();
DialogUtilities.okDialog(EditPreferences.this, "done", null);
return false;
}
});
group.addPreference(preference);
}
@Override

@ -3,15 +3,19 @@
*/
package com.todoroo.astrid.activity;
import java.io.IOException;
import org.json.JSONException;
import android.app.AlertDialog;
import android.app.ExpandableListActivity;
import android.app.PendingIntent.CanceledException;
import android.app.ProgressDialog;
import android.app.SearchManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
@ -23,7 +27,6 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ExpandableListView;
@ -40,6 +43,9 @@ import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.actfm.sync.ActFmSyncService;
import com.todoroo.astrid.adapter.FilterAdapter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterCategory;
@ -65,16 +71,18 @@ public class FilterListActivity extends ExpandableListActivity {
private static final int MENU_SEARCH_ID = Menu.FIRST + 0;
private static final int MENU_HELP_ID = Menu.FIRST + 1;
private static final int MENU_REFRESH_ID = Menu.FIRST + 2;
private static final int CONTEXT_MENU_SHORTCUT = Menu.FIRST + 2;
private static final int CONTEXT_MENU_INTENT = Menu.FIRST + 3;
private static final int CONTEXT_MENU_SHORTCUT = Menu.FIRST + 3;
private static final int CONTEXT_MENU_INTENT = Menu.FIRST + 4;
private static final int REQUEST_CUSTOM_INTENT = 1;
// --- instance variables
@Autowired
protected ExceptionService exceptionService;
@Autowired ExceptionService exceptionService;
@Autowired ActFmPreferenceService actFmPreferenceService;
@Autowired ActFmSyncService actFmSyncService;
FilterAdapter adapter = null;
@ -99,10 +107,6 @@ public class FilterListActivity extends ExpandableListActivity {
setTitle(R.string.FLA_title);
onNewIntent(getIntent());
// dithering
getWindow().setFormat(PixelFormat.RGBA_8888);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DITHER);
}
/**
@ -147,6 +151,12 @@ public class FilterListActivity extends ExpandableListActivity {
R.string.FLA_menu_search);
item.setIcon(android.R.drawable.ic_menu_search);
if(actFmPreferenceService.isLoggedIn()) {
item = menu.add(Menu.NONE, MENU_REFRESH_ID, Menu.NONE,
R.string.actfm_FLA_menu_refresh);
item.setIcon(R.drawable.ic_menu_refresh);
}
item = menu.add(Menu.NONE, MENU_HELP_ID, Menu.NONE,
R.string.FLA_menu_help);
item.setIcon(android.R.drawable.ic_menu_help);
@ -213,7 +223,7 @@ public class FilterListActivity extends ExpandableListActivity {
FilterWithCustomIntent customFilter = ((FilterWithCustomIntent)filter);
intent.setComponent(customFilter.customTaskList);
if(customFilter.customExtras != null)
intent.getExtras().putAll(customFilter.customExtras);
intent.putExtras(customFilter.customExtras);
}
startActivity(intent);
AndroidUtilities.callApiMethod(5, this, "overridePendingTransition", //$NON-NLS-1$
@ -344,6 +354,11 @@ public class FilterListActivity extends ExpandableListActivity {
return true;
}
case MENU_REFRESH_ID: {
onRefreshRequested();
return true;
}
case MENU_HELP_ID: {
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("http://weloveastrid.com/help-user-guide-astrid-v3/filters/")); //$NON-NLS-1$
@ -372,6 +387,33 @@ public class FilterListActivity extends ExpandableListActivity {
return false;
}
/**
* Refresh user tags
*/
private void onRefreshRequested() {
final ProgressDialog progressDialog = DialogUtilities.progressDialog(this, getString(R.string.DLG_please_wait));
new Thread(new Runnable() {
@SuppressWarnings("nls")
@Override
public void run() {
try {
actFmSyncService.fetchTags();
adapter.clear();
adapter.getLists();
} catch (IOException e) {
exceptionService.displayAndReportError(FilterListActivity.this, "refresh-tags-io", e);
} catch (JSONException e) {
exceptionService.displayAndReportError(FilterListActivity.this, "refresh-tags-json", e);
} finally {
progressDialog.dismiss();
}
}
}).start();
}
private void showCreateShortcutDialog(final Intent shortcutIntent,
final Filter filter) {
FrameLayout frameLayout = new FrameLayout(this);

@ -26,7 +26,7 @@ public final class ShareLinkActivity extends TaskListActivity {
String subject = callerIntent.getStringExtra(Intent.EXTRA_SUBJECT);
if(subject == null)
subject = "";
subject = ""; //$NON-NLS-1$
Task task = quickAddTask(subject, false);
task.setValue(Task.NOTES, callerIntent.getStringExtra(Intent.EXTRA_TEXT));
taskService.save(task);

@ -144,11 +144,9 @@ public class ShortcutActivity extends Activity {
if(filter instanceof FilterWithCustomIntent) {
FilterWithCustomIntent customFilter = ((FilterWithCustomIntent)filter);
if(customFilter.customExtras != null)
shortcutIntent.putExtras(customFilter.customExtras);
shortcutIntent.putExtra(TOKEN_CUSTOM_CLASS, customFilter.customTaskList.flattenToString());
if(customFilter.customExtras != null) {
for(String key : customFilter.customExtras.keySet())
putExtra(shortcutIntent, key, customFilter.customExtras.get(key));
}
}
shortcutIntent.setAction(Intent.ACTION_VIEW);
@ -160,7 +158,6 @@ public class ShortcutActivity extends Activity {
for (Entry<String, Object> item : filter.valuesForNewTasks.valueSet()) {
String key = TOKEN_FILTER_VALUES_ITEM + item.getKey();
Object value = item.getValue();
putExtra(shortcutIntent, key, value);
}
}

@ -121,20 +121,20 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
// --- menu codes
private static final int MENU_ADDONS_ID = Menu.FIRST + 1;
private static final int MENU_SETTINGS_ID = Menu.FIRST + 2;
private static final int MENU_SORT_ID = Menu.FIRST + 3;
private static final int MENU_SYNC_ID = Menu.FIRST + 4;
private static final int MENU_HELP_ID = Menu.FIRST + 5;
private static final int MENU_ADDON_INTENT_ID = Menu.FIRST + 6;
private static final int CONTEXT_MENU_EDIT_TASK_ID = Menu.FIRST + 20;
private static final int CONTEXT_MENU_COPY_TASK_ID = Menu.FIRST + 21;
private static final int CONTEXT_MENU_DELETE_TASK_ID = Menu.FIRST + 22;
private static final int CONTEXT_MENU_UNDELETE_TASK_ID = Menu.FIRST + 23;
private static final int CONTEXT_MENU_PURGE_TASK_ID = Menu.FIRST + 24;
private static final int CONTEXT_MENU_BROADCAST_INTENT_ID = Menu.FIRST + 25;
private static final int CONTEXT_MENU_PLUGIN_ID_FIRST = Menu.FIRST + 26;
protected static final int MENU_ADDONS_ID = Menu.FIRST + 1;
protected static final int MENU_SETTINGS_ID = Menu.FIRST + 2;
protected static final int MENU_SORT_ID = Menu.FIRST + 3;
protected static final int MENU_SYNC_ID = Menu.FIRST + 4;
protected static final int MENU_HELP_ID = Menu.FIRST + 5;
protected static final int MENU_ADDON_INTENT_ID = Menu.FIRST + 6;
protected static final int CONTEXT_MENU_EDIT_TASK_ID = Menu.FIRST + 20;
protected static final int CONTEXT_MENU_COPY_TASK_ID = Menu.FIRST + 21;
protected static final int CONTEXT_MENU_DELETE_TASK_ID = Menu.FIRST + 22;
protected static final int CONTEXT_MENU_UNDELETE_TASK_ID = Menu.FIRST + 23;
protected static final int CONTEXT_MENU_PURGE_TASK_ID = Menu.FIRST + 24;
protected static final int CONTEXT_MENU_BROADCAST_INTENT_ID = Menu.FIRST + 25;
protected static final int CONTEXT_MENU_PLUGIN_ID_FIRST = Menu.FIRST + 26;
// --- constants
@ -657,7 +657,6 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
protected void setUpTaskList() {
sqlQueryTemplate.set(SortHelper.adjustQueryForFlagsAndSort(filter.sqlQuery,
sortFlags, sortSort));
((TextView)findViewById(R.id.listLabel)).setText(filter.title);
// perform query

@ -107,6 +107,8 @@ public class FilterAdapter extends BaseExpandableListAdapter {
@Override
public void run() {
try {
if(filter.listingTitle.matches(".* \\(\\d\\)$")) //$NON-NLS-1$
return;
int size = taskService.countTasks(filter);
filter.listingTitle = filter.listingTitle + (" (" + //$NON-NLS-1$
size + ")"); //$NON-NLS-1$

@ -1,5 +1,6 @@
package com.todoroo.astrid.adapter;
import greendroid.widget.AsyncImageView;
import greendroid.widget.QuickAction;
import greendroid.widget.QuickActionBar;
import greendroid.widget.QuickActionWidget;
@ -13,6 +14,9 @@ import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
@ -28,6 +32,7 @@ import android.text.Html.TagHandler;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
@ -97,7 +102,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
Task.DETAILS,
Task.ELAPSED_SECONDS,
Task.TIMER_START,
Task.NOTES
Task.NOTES,
Task.USER_ID,
Task.USER
};
private static int[] IMPORTANCE_COLORS = null;
@ -205,6 +212,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
viewHolder.task = new Task();
viewHolder.view = view;
viewHolder.nameView = (TextView)view.findViewById(R.id.title);
viewHolder.picture = (AsyncImageView)view.findViewById(R.id.picture);
viewHolder.completeBox = (CheckBox)view.findViewById(R.id.completeBox);
viewHolder.dueDate = (TextView)view.findViewById(R.id.dueDate);
viewHolder.details = (TextView)view.findViewById(R.id.details);
@ -261,6 +269,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
public View view;
public TextView nameView;
public CheckBox completeBox;
public AsyncImageView picture;
public TextView dueDate;
public TextView details;
public TextView extendedDetails;
@ -324,6 +333,22 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
completeBox.setEnabled(!viewHolder.task.getFlag(Task.FLAGS, Task.FLAG_IS_READONLY));
}
// image view
final AsyncImageView pictureView = viewHolder.picture; {
if(task.getValue(Task.USER_ID) == 0) {
pictureView.setVisibility(View.GONE);
} else {
pictureView.setVisibility(View.VISIBLE);
pictureView.setUrl(null);
try {
JSONObject user = new JSONObject(task.getValue(Task.USER));
pictureView.setUrl(user.optString("picture")); //$NON-NLS-1$
} catch (JSONException e) {
Log.w("astrid", "task-adapter-image", e); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
// importance bar
final View importanceView = viewHolder.importance; {
int value = task.getValue(Task.IMPORTANCE);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save