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