Merge remote branch 'origin/master' into dev

Conflicts:
	astrid/AndroidManifest.xml
	astrid/plugin-src/com/todoroo/astrid/producteev/sync/ProducteevDataService.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/MilkBackgroundService.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/MilkDetailExposer.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/api/data/RtmTask.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/data/MilkNote.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/sync/RTMSyncProvider.java
	astrid/plugin-src/com/todoroo/astrid/rmilk/sync/RTMTaskContainer.java
	astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
	astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java
	astrid/src/com/todoroo/astrid/service/StartupService.java
pull/14/head
Tim Su 16 years ago
commit 33ae2cad02

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid" package="com.timsu.astrid"
android:versionName="3.2.5 (build your own filters, easy sorting, customizable widget, ui improvements)" android:versionName="3.2.7 (bug fixes and ui improvements)"
android:versionCode="152"> android:versionCode="154">
<!-- widgets, alarms, and services will break if Astrid is installed on SD card --> <!-- widgets, alarms, and services will break if Astrid is installed on SD card -->
<!-- android:installLocation="internalOnly"> --> <!-- android:installLocation="internalOnly"> -->
@ -196,6 +196,12 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="com.todoroo.astrid.alarms.AlarmDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- tags --> <!-- tags -->
<receiver android:name="com.todoroo.astrid.tags.TagsPlugin"> <receiver android:name="com.todoroo.astrid.tags.TagsPlugin">
@ -210,6 +216,12 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="com.todoroo.astrid.tags.TagDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- repeats --> <!-- repeats -->
<receiver android:name="com.todoroo.astrid.repeats.RepeatsPlugin"> <receiver android:name="com.todoroo.astrid.repeats.RepeatsPlugin">
@ -224,6 +236,12 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="com.todoroo.astrid.repeats.RepeatDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- calendar --> <!-- calendar -->
<receiver android:name="com.todoroo.astrid.gcal.GCalTaskCompleteListener"> <receiver android:name="com.todoroo.astrid.gcal.GCalTaskCompleteListener">
@ -262,6 +280,12 @@
</receiver> </receiver>
<!-- notes --> <!-- notes -->
<receiver android:name="com.todoroo.astrid.notes.NoteDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- locale --> <!-- locale -->
<activity android:name="com.todoroo.astrid.locale.LocaleEditAlerts" <activity android:name="com.todoroo.astrid.locale.LocaleEditAlerts"
@ -323,6 +347,7 @@
android:taskAffinity="com.todoroo.astrid.reminders.NotificationActivity" android:taskAffinity="com.todoroo.astrid.reminders.NotificationActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:finishOnTaskLaunch="true"
android:clearTaskOnLaunch="true" /> android:clearTaskOnLaunch="true" />
<!-- producteev --> <!-- producteev -->
@ -336,7 +361,7 @@
android:icon="@drawable/ic_menu_producteev" android:icon="@drawable/ic_menu_producteev"
android:label="@string/producteev_PPr_header"> android:label="@string/producteev_PPr_header">
<intent-filter> <intent-filter>
<action android:name="com.todoroo.astrid.TASK_LIST_MENU" /> <action android:name="com.todoroo.astrid.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
@ -344,11 +369,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="com.todoroo.astrid.producteev.ProducteevLoginActivity" <activity android:name="com.todoroo.astrid.producteev.ProducteevLoginActivity"
android:theme="@style/Theme"> android:theme="@style/Theme" />
<intent-filter> <!-- temporary -->
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<service android:name="com.todoroo.astrid.producteev.ProducteevBackgroundService"/> <service android:name="com.todoroo.astrid.producteev.ProducteevBackgroundService"/>
<receiver android:name="com.todoroo.astrid.producteev.ProducteevStartupReceiver"> <receiver android:name="com.todoroo.astrid.producteev.ProducteevStartupReceiver">
<intent-filter> <intent-filter>
@ -356,6 +377,18 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="com.todoroo.astrid.producteev.ProducteevDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.producteev.ProducteevSyncActionExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_SYNC_ACTIONS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
</application> </application>

@ -69,6 +69,11 @@ public class AstridApiConstants {
*/ */
public static final String EXTRAS_NEW_DUE_DATE = "newDueDate"; public static final String EXTRAS_NEW_DUE_DATE = "newDueDate";
/**
* Extras name for sync provider name
*/
public static final String EXTRAS_NAME = "name";
// --- Add-ons API // --- Add-ons API
/** /**
@ -131,6 +136,21 @@ public class AstridApiConstants {
*/ */
public static final String BROADCAST_SEND_DETAILS = PACKAGE + ".SEND_DETAILS"; public static final String BROADCAST_SEND_DETAILS = PACKAGE + ".SEND_DETAILS";
// --- Sync Provider API
/**
* Action name for broadcast intent requesting a listing of active
* sync actions users can activate from the menu
*/
public static final String BROADCAST_REQUEST_SYNC_ACTIONS = PACKAGE + ".REQUEST_SYNC_ACTIONS";
/**
* Action name for broadcast intent sending sync provider information back to Astrid
* @extra EXTRAS_ADDON your add-on identifier
* @extra EXTRAS_RESPONSE a {@link SyncAction} to invoke synchronization
*/
public static final String BROADCAST_SEND_SYNC_ACTIONS = PACKAGE + ".SEND_SYNC_ACTIONS";
// --- Task Actions API // --- Task Actions API
/** /**

@ -0,0 +1,87 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.api;
import android.app.PendingIntent;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Represents an intent that can be called to perform synchronization
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class SyncAction implements Parcelable {
/**
* Label
*/
public String label = null;
/**
* Intent to call when invoking this operation
*/
public PendingIntent intent = null;
/**
* Create an EditOperation object
*
* @param label
* label to display
* @param intent
* intent to invoke. {@link EXTRAS_TASK_ID} will be passed
*/
public SyncAction(String label, PendingIntent intent) {
super();
this.label = label;
this.intent = intent;
}
/**
* Returns the label of this action
*/
@Override
public String toString() {
return label;
}
// --- parcelable helpers
/**
* {@inheritDoc}
*/
public int describeContents() {
return 0;
}
/**
* {@inheritDoc}
*/
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(label);
dest.writeParcelable(intent, 0);
}
/**
* Parcelable creator
*/
public static final Parcelable.Creator<SyncAction> CREATOR = new Parcelable.Creator<SyncAction>() {
/**
* {@inheritDoc}
*/
public SyncAction createFromParcel(Parcel source) {
return new SyncAction(source.readString(), (PendingIntent)source.readParcelable(
PendingIntent.class.getClassLoader()));
}
/**
* {@inheritDoc}
*/
public SyncAction[] newArray(int size) {
return new SyncAction[size];
};
};
}

@ -12,7 +12,7 @@
<intAttribute key="com.android.ide.eclipse.adt.delay" value="0"/> <intAttribute key="com.android.ide.eclipse.adt.delay" value="0"/>
<booleanAttribute key="com.android.ide.eclipse.adt.nobootanim" value="true"/> <booleanAttribute key="com.android.ide.eclipse.adt.nobootanim" value="true"/>
<intAttribute key="com.android.ide.eclipse.adt.speed" value="0"/> <intAttribute key="com.android.ide.eclipse.adt.speed" value="0"/>
<booleanAttribute key="com.android.ide.eclipse.adt.target" value="true"/> <booleanAttribute key="com.android.ide.eclipse.adt.target" value="false"/>
<booleanAttribute key="com.android.ide.eclipse.adt.wipedata" value="false"/> <booleanAttribute key="com.android.ide.eclipse.adt.wipedata" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/astrid"/> <listEntry value="/astrid"/>

@ -9,8 +9,8 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log; import android.util.Log;
import com.todoroo.andlib.data.Property.PropertyVisitor; import com.todoroo.andlib.data.Property.PropertyVisitor;

@ -337,6 +337,11 @@ public abstract class AbstractModel implements Parcelable {
public synchronized void save(Property<?> property, ContentValues newStore, Object value) { public synchronized void save(Property<?> property, ContentValues newStore, Object value) {
this.store = newStore; this.store = newStore;
// we don't allow null values, as they indicate unset properties
// when the database was written
if(value != null)
property.accept(this, value); property.accept(this, value);
} }

@ -212,6 +212,20 @@ public class GenericDao<TYPE extends AbstractModel> {
return result; return result;
} }
/**
* Updates multiple rows of the database based on model set values
*
* @param item
* item model
* @param criterion
* @return returns true on success.
*/
public int updateMultiple(ContentValues values, Criterion criterion) {
if(values.size() == 0) // nothing changed
return 0;
return database.update(table.name, values, criterion.toString(), null);
}
// --- helper methods // --- helper methods

@ -8,6 +8,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map.Entry; import java.util.Map.Entry;
import android.app.Activity; import android.app.Activity;
@ -23,8 +25,8 @@ import android.text.InputType;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnTouchListener; import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.Autowired;
@ -357,6 +359,18 @@ public class AndroidUtilities {
} }
} }
/**
* Sort files by date so the newest file is on top
* @param files
*/
public static void sortFilesByDateDesc(File[] files) {
Arrays.sort(files, new Comparator<File>() {
public int compare(File o1, File o2) {
return Long.valueOf(o2.lastModified()).compareTo(Long.valueOf(o1.lastModified()));
}
});
}
/** /**
* Sleep, ignoring interruption * Sleep, ignoring interruption
* @param l * @param l

@ -32,6 +32,9 @@ public class DialogUtilities {
public void viewDialog(final Activity activity, final String text, public void viewDialog(final Activity activity, final String text,
final View view, final DialogInterface.OnClickListener okListener, final View view, final DialogInterface.OnClickListener okListener,
final DialogInterface.OnClickListener cancelListener) { final DialogInterface.OnClickListener cancelListener) {
if(activity.isFinishing())
return;
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
@ -55,6 +58,9 @@ public class DialogUtilities {
*/ */
public void okDialog(final Activity activity, final String text, public void okDialog(final Activity activity, final String text,
final DialogInterface.OnClickListener okListener) { final DialogInterface.OnClickListener okListener) {
if(activity.isFinishing())
return;
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
@ -76,6 +82,9 @@ public class DialogUtilities {
*/ */
public void okDialog(final Activity activity, final int icon, final CharSequence text, public void okDialog(final Activity activity, final int icon, final CharSequence text,
final DialogInterface.OnClickListener okListener) { final DialogInterface.OnClickListener okListener) {
if(activity.isFinishing())
return;
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
@ -100,6 +109,9 @@ public class DialogUtilities {
public void okCancelDialog(final Activity activity, final String title, public void okCancelDialog(final Activity activity, final String title,
final String text, final DialogInterface.OnClickListener okListener, final String text, final DialogInterface.OnClickListener okListener,
final DialogInterface.OnClickListener cancelListener) { final DialogInterface.OnClickListener cancelListener) {
if(activity.isFinishing())
return;
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
@ -124,6 +136,9 @@ public class DialogUtilities {
public void okCancelDialog(final Activity activity, final String text, public void okCancelDialog(final Activity activity, final String text,
final DialogInterface.OnClickListener okListener, final DialogInterface.OnClickListener okListener,
final DialogInterface.OnClickListener cancelListener) { final DialogInterface.OnClickListener cancelListener) {
if(activity.isFinishing())
return;
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)

@ -34,7 +34,7 @@ public class Api4GestureDetector implements OnGesturePerformedListener {
if (predictions.size() > 0) { if (predictions.size() > 0) {
Prediction prediction = predictions.get(0); Prediction prediction = predictions.get(0);
// We want at least some confidence in the result // We want at least some confidence in the result
if (prediction.score > 1.0) { if (prediction.score > 2.0) {
listener.gesturePerformed(prediction.name); listener.gesturePerformed(prediction.name);
} }
} }

@ -12,7 +12,6 @@ import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
/** /**
@ -21,7 +20,7 @@ import com.todoroo.astrid.model.Metadata;
* @author Tim Su <tim@todoroo.com> * @author Tim Su <tim@todoroo.com>
* *
*/ */
public class AlarmDetailExposer extends BroadcastReceiver implements DetailExposer { public class AlarmDetailExposer extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -44,7 +43,6 @@ public class AlarmDetailExposer extends BroadcastReceiver implements DetailExpos
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
} }
@Override
public String getTaskDetails(Context context, long id, boolean extended) { public String getTaskDetails(Context context, long id, boolean extended) {
if(extended) if(extended)
return null; return null;
@ -71,9 +69,4 @@ public class AlarmDetailExposer extends BroadcastReceiver implements DetailExpos
} }
} }
@Override
public String getPluginIdentifier() {
return AlarmService.IDENTIFIER;
}
} }

@ -2,9 +2,6 @@ package com.todoroo.astrid.backup;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
@ -12,6 +9,7 @@ import android.content.DialogInterface;
import android.util.Log; import android.util.Log;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.andlib.utility.AndroidUtilities;
@SuppressWarnings("nls") @SuppressWarnings("nls")
public class FilePickerBuilder extends AlertDialog.Builder implements DialogInterface.OnClickListener { public class FilePickerBuilder extends AlertDialog.Builder implements DialogInterface.OnClickListener {
@ -46,11 +44,14 @@ public class FilePickerBuilder extends AlertDialog.Builder implements DialogInte
private void setPath(File path) { private void setPath(File path) {
if (path != null && path.exists()) { if (path != null && path.exists()) {
this.path = path.getAbsolutePath(); this.path = path.getAbsolutePath();
// Reverse the order of the file list so newest time-stamped file is first.
List<String> fileList = Arrays.asList(path.list(filter)); File[] filesAsFile = path.listFiles(filter);
Collections.sort(fileList); AndroidUtilities.sortFilesByDateDesc(filesAsFile);
Collections.reverse(fileList);
files = (String[]) fileList.toArray(); files = new String[filesAsFile.length];
for(int i = 0; i < files.length; i++)
files[i] = filesAsFile[i].getName();
setItems(files, this); setItems(files, this);
} else { } else {
Log.e("FilePicker", "Cannot access sdcard."); Log.e("FilePicker", "Cannot access sdcard.");

@ -50,7 +50,7 @@ public class TasksXmlExporter {
private static final int FORMAT = 2; private static final int FORMAT = 2;
private final Context context; private final Context context;
private int exportCount; private int exportCount = 0;
private XmlSerializer xml; private XmlSerializer xml;
private final TaskService taskService = PluginServices.getTaskService(); private final TaskService taskService = PluginServices.getTaskService();
private final MetadataService metadataService = PluginServices.getMetadataService(); private final MetadataService metadataService = PluginServices.getMetadataService();
@ -93,7 +93,11 @@ public class TasksXmlExporter {
try { try {
String output = setupFile(BackupConstants.getExportDirectory(), String output = setupFile(BackupConstants.getExportDirectory(),
isService); isService);
int tasks = taskService.countTasks();
if(tasks > 0)
doTasksExport(output); doTasksExport(output);
Preferences.setLong(BackupPreferences.PREF_BACKUP_LAST_DATE, Preferences.setLong(BackupPreferences.PREF_BACKUP_LAST_DATE,
DateUtilities.now()); DateUtilities.now());
Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, null); Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, null);
@ -270,6 +274,10 @@ public class TasksXmlExporter {
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
if(exportCount == 0)
Toast.makeText(context, context.getString(R.string.export_toast_no_tasks), Toast.LENGTH_LONG).show();
else {
CharSequence text = String.format(context.getString(R.string.export_toast), CharSequence text = String.format(context.getString(R.string.export_toast),
context.getResources().getQuantityString(R.plurals.Ntasks, exportCount, context.getResources().getQuantityString(R.plurals.Ntasks, exportCount,
exportCount), outputFile); exportCount), outputFile);
@ -277,6 +285,7 @@ public class TasksXmlExporter {
if(progressDialog.isShowing()) if(progressDialog.isShowing())
progressDialog.dismiss(); progressDialog.dismiss();
} }
}
}); });
} }

@ -41,6 +41,7 @@ import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.utility.Flags;
public class TasksXmlImporter { public class TasksXmlImporter {
@ -145,6 +146,7 @@ public class TasksXmlImporter {
} }
} }
} finally { } finally {
Flags.set(Flags.REFRESH);
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {

@ -11,20 +11,16 @@ import java.util.HashMap;
import android.app.Activity; import android.app.Activity;
import android.app.Notification; import android.app.Notification;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.widget.Toast; import android.widget.Toast;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.data.Property.LongProperty; import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.service.NotificationManager; import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.astrid.api.TaskContainer;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Flags;
/** /**
* A helper class for writing synchronization services for Astrid. This class * A helper class for writing synchronization services for Astrid. This class
@ -42,13 +38,23 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
// --- abstract methods - your services should implement these // --- abstract methods - your services should implement these
/** /**
* Perform authenticate and other pre-synchronization steps, then * Perform log in (launching activity if necessary) and sync. This is
* synchronize. * invoked when users manually request synchronization
* *
* @param context * @param activity
* either the parent activity, or a background service * context
*/
abstract protected void initiateManual(Activity activity);
/**
* Perform synchronize. Since this can be called from background services,
* you should not open up new activities. Instead, if the user is not signed
* in, your service should do nothing.
*
* @param service
* context
*/ */
abstract protected void initiate(Context context); abstract protected void initiateBackground(Service service);
/** /**
* Updates the text of a notification and the intent to open when tapped * Updates the text of a notification and the intent to open when tapped
@ -128,14 +134,9 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
// --- implementation // --- implementation
@Autowired
private ExceptionService exceptionService;
private final Notification notification; private final Notification notification;
public SyncProvider() { public SyncProvider() {
DependencyInjectionService.getInstance().inject(this);
// initialize notification // initialize notification
int icon = android.R.drawable.stat_notify_sync; int icon = android.R.drawable.stat_notify_sync;
long when = System.currentTimeMillis(); long when = System.currentTimeMillis();
@ -153,8 +154,8 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
} }
}); });
} initiateManual((Activity)context);
} else if(context instanceof Service) {
// display notification // display notification
updateNotification(context, notification); updateNotification(context, notification);
final NotificationManager nm = new NotificationManager.AndroidNotificationManager(context); final NotificationManager nm = new NotificationManager.AndroidNotificationManager(context);
@ -164,23 +165,13 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
try { try {
initiate(context); initiateBackground((Service)context);
} finally { } finally {
nm.cancel(Constants.NOTIFICATION_SYNC); nm.cancel(Constants.NOTIFICATION_SYNC);
} }
} }
}).start(); }).start();
} }
// --- utilities
/**
* Utility method for showing synchronization errors. If message is null,
* the contents of the throwable is displayed. It is assumed that the error
* was logged separately.
*/
protected void showError(final Context context, Throwable e, String message) {
exceptionService.displayAndReportError(context, message, e);
} }
// --- synchronization logic // --- synchronization logic
@ -304,14 +295,18 @@ public abstract class SyncProvider<TYPE extends TaskContainer> {
length = data.remoteUpdated.size(); length = data.remoteUpdated.size();
for(int i = 0; i < length; i++) { for(int i = 0; i < length; i++) {
TYPE remote = data.remoteUpdated.get(i); TYPE remote = data.remoteUpdated.get(i);
// don't synchronize new & deleted / completed tasks
if(!remote.task.isSaved() && (remote.task.isDeleted() ||
remote.task.isCompleted()))
continue;
try { try {
write(remote); write(remote);
} catch (Exception e) { } catch (Exception e) {
handleException("sync-remote-updated", e, false); //$NON-NLS-1$ handleException("sync-remote-updated", e, false); //$NON-NLS-1$
} }
} }
Flags.set(Flags.REFRESH);
} }
// --- helper classes // --- helper classes

@ -1,9 +1,8 @@
package com.todoroo.astrid.api; package com.todoroo.astrid.common;
import java.util.ArrayList; import java.util.ArrayList;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.common.SyncProvider;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;

@ -17,6 +17,7 @@ import android.widget.Spinner;
import com.flurry.android.FlurryAgent; import com.flurry.android.FlurryAgent;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.activity.AddOnActivity; import com.todoroo.astrid.activity.AddOnActivity;
import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.adapter.FilterAdapter;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
@ -44,6 +45,10 @@ public final class LocaleEditAlerts extends ExpandableListActivity {
@SuppressWarnings("nls") @SuppressWarnings("nls")
public static final String KEY_SQL = "sql"; public static final String KEY_SQL = "sql";
/** key name for filter content-values in bundle */
@SuppressWarnings("nls")
public static final String KEY_VALUES = "val";
/** key name for interval (integer, # of seconds) */ /** key name for interval (integer, # of seconds) */
@SuppressWarnings("nls") @SuppressWarnings("nls")
public static final String KEY_INTERVAL = "interval"; public static final String KEY_INTERVAL = "interval";
@ -262,6 +267,8 @@ public final class LocaleEditAlerts extends ExpandableListActivity {
Filter filterItem = (Filter) selected; Filter filterItem = (Filter) selected;
storeAndForwardExtras.putString(KEY_FILTER_TITLE, filterItem.title); storeAndForwardExtras.putString(KEY_FILTER_TITLE, filterItem.title);
storeAndForwardExtras.putString(KEY_SQL, filterItem.sqlQuery); storeAndForwardExtras.putString(KEY_SQL, filterItem.sqlQuery);
if(filterItem.valuesForNewTasks != null)
storeAndForwardExtras.putString(KEY_VALUES, AndroidUtilities.contentValuesToSerializedString(filterItem.valuesForNewTasks));
storeAndForwardExtras.putInt(KEY_INTERVAL, INTERVALS[intervalIndex]); storeAndForwardExtras.putInt(KEY_INTERVAL, INTERVALS[intervalIndex]);
returnIntent.putExtra(com.twofortyfouram.Intent.EXTRA_BUNDLE, storeAndForwardExtras); returnIntent.putExtra(com.twofortyfouram.Intent.EXTRA_BUNDLE, storeAndForwardExtras);

@ -11,6 +11,7 @@ import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.activity.ShortcutActivity; import com.todoroo.astrid.activity.ShortcutActivity;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
@ -51,6 +52,7 @@ public class LocaleReceiver extends BroadcastReceiver {
final String title = intent.getStringExtra(LocaleEditAlerts.KEY_FILTER_TITLE); final String title = intent.getStringExtra(LocaleEditAlerts.KEY_FILTER_TITLE);
final String sql = intent.getStringExtra(LocaleEditAlerts.KEY_SQL); final String sql = intent.getStringExtra(LocaleEditAlerts.KEY_SQL);
final String values = intent.getStringExtra(LocaleEditAlerts.KEY_VALUES);
final int interval = intent.getIntExtra(LocaleEditAlerts.KEY_INTERVAL, 24*3600); final int interval = intent.getIntExtra(LocaleEditAlerts.KEY_INTERVAL, 24*3600);
if(TextUtils.isEmpty(title) || TextUtils.isEmpty(sql) || if(TextUtils.isEmpty(title) || TextUtils.isEmpty(sql) ||
@ -74,6 +76,10 @@ public class LocaleReceiver extends BroadcastReceiver {
try { try {
if(cursor.getCount() == 0) if(cursor.getCount() == 0)
return; return;
if(values != null)
filter.valuesForNewTasks = AndroidUtilities.contentValuesFromSerializedString(values);
Resources r = context.getResources(); Resources r = context.getResources();
String reminder = r.getString(R.string.locale_notification). String reminder = r.getString(R.string.locale_notification).
replace("$NUM", r.getQuantityString(R.plurals.Ntasks, replace("$NUM", r.getQuantityString(R.plurals.Ntasks,

@ -10,7 +10,6 @@ import android.content.Intent;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.utility.Preferences; import com.todoroo.astrid.utility.Preferences;
@ -21,7 +20,7 @@ import com.todoroo.astrid.utility.Preferences;
* @author Tim Su <tim@todoroo.com> * @author Tim Su <tim@todoroo.com>
* *
*/ */
public class NoteDetailExposer extends BroadcastReceiver implements DetailExposer { public class NoteDetailExposer extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -31,7 +30,7 @@ public class NoteDetailExposer extends BroadcastReceiver implements DetailExpose
return; return;
boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false);
String taskDetail = getTaskDetails(context, taskId, extended); String taskDetail = getTaskDetails(taskId, extended);
if(taskDetail == null) if(taskDetail == null)
return; return;
@ -44,8 +43,7 @@ public class NoteDetailExposer extends BroadcastReceiver implements DetailExpose
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
} }
@Override public String getTaskDetails(long id, boolean extended) {
public String getTaskDetails(Context context, long id, boolean extended) {
if(Preferences.getBoolean(R.string.p_showNotes, false)) { if(Preferences.getBoolean(R.string.p_showNotes, false)) {
if(extended) if(extended)
@ -65,9 +63,4 @@ public class NoteDetailExposer extends BroadcastReceiver implements DetailExpose
return "<img src='silk_note'/> " + notes; //$NON-NLS-1$ return "<img src='silk_note'/> " + notes; //$NON-NLS-1$
} }
@Override
public String getPluginIdentifier() {
return NotesPlugin.IDENTIFIER;
}
} }

@ -39,6 +39,7 @@ public class ProducteevBackgroundService extends Service {
@Override @Override
public void onStart(Intent intent, int startId) { public void onStart(Intent intent, int startId) {
try { try {
if(intent != null && SYNC_ACTION.equals(intent.getAction()))
startSynchronization(this); startSynchronization(this);
} catch (Exception e) { } catch (Exception e) {
PluginServices.getExceptionService().reportError("pdv-bg-sync", e); //$NON-NLS-1$ PluginServices.getExceptionService().reportError("pdv-bg-sync", e); //$NON-NLS-1$

@ -2,7 +2,12 @@ package com.todoroo.astrid.producteev;
import java.util.ArrayList; import java.util.ArrayList;
import org.json.JSONObject;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -10,6 +15,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener; import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
@ -17,12 +23,14 @@ import com.timsu.astrid.R;
import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.activity.TaskEditActivity.TaskEditControlSet; import com.todoroo.astrid.activity.TaskEditActivity.TaskEditControlSet;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.StoreObject; import com.todoroo.astrid.model.StoreObject;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard; import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
import com.todoroo.astrid.producteev.sync.ProducteevDataService; import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider;
import com.todoroo.astrid.producteev.sync.ProducteevTask; import com.todoroo.astrid.producteev.sync.ProducteevTask;
import com.todoroo.astrid.producteev.sync.ProducteevUser; import com.todoroo.astrid.producteev.sync.ProducteevUser;
import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.MetadataService;
@ -38,6 +46,7 @@ public class ProducteevControlSet implements TaskEditControlSet {
// --- instance variables // --- instance variables
private final Activity activity; private final Activity activity;
private final DialogUtilities dialogUtilites;
private final View view; private final View view;
private Task myTask; private Task myTask;
@ -50,10 +59,14 @@ public class ProducteevControlSet implements TaskEditControlSet {
@Autowired @Autowired
MetadataService metadataService; MetadataService metadataService;
private int lastDashboardSelection = 0;
public ProducteevControlSet(final Activity activity, ViewGroup parent) { public ProducteevControlSet(final Activity activity, ViewGroup parent) {
DependencyInjectionService.getInstance().inject(this); DependencyInjectionService.getInstance().inject(this);
this.activity = activity; this.activity = activity;
this.dialogUtilites = new DialogUtilities();
view = LayoutInflater.from(activity).inflate(R.layout.producteev_control, parent, true); view = LayoutInflater.from(activity).inflate(R.layout.producteev_control, parent, true);
this.responsibleSelector = (Spinner) activity.findViewById(R.id.producteev_TEA_task_assign); this.responsibleSelector = (Spinner) activity.findViewById(R.id.producteev_TEA_task_assign);
@ -67,9 +80,68 @@ public class ProducteevControlSet implements TaskEditControlSet {
@Override @Override
public void onItemSelected(AdapterView<?> spinnerParent, View spinnerView, public void onItemSelected(AdapterView<?> spinnerParent, View spinnerView,
int position, long id) { int position, long id) {
Spinner dashSelector = (Spinner) spinnerParent; final Spinner dashSelector = (Spinner) spinnerParent;
ProducteevDashboard dashboard = (ProducteevDashboard) dashSelector.getSelectedItem(); ProducteevDashboard dashboard = (ProducteevDashboard) dashSelector.getSelectedItem();
if (dashboard.getId() == ProducteevUtilities.DASHBOARD_CREATE) {
// let the user create a new dashboard
final EditText editor = new EditText(ProducteevControlSet.this.activity);
OnClickListener okListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Activity context = ProducteevControlSet.this.activity;
String newDashboardName = editor.getText().toString();
if (newDashboardName == null || newDashboardName.length() == 0) {
dialog.cancel();
} else {
// create the real dashboard, select it in the spinner and refresh responsiblespinner
ProgressDialog progressDialog = dialogUtilites.progressDialog(context,
context.getString(R.string.DLG_wait));
try {
progressDialog.show();
JSONObject newDashJSON = ProducteevSyncProvider.getInvoker().dashboardsCreate(
newDashboardName).getJSONObject("dashboard"); //$NON-NLS-1$
StoreObject local = ProducteevDataService.getInstance().updateDashboards(newDashJSON, true);
if (local != null) {
ProducteevDashboard newDashboard = new ProducteevDashboard(local);
ArrayAdapter<ProducteevDashboard> adapter = (ArrayAdapter<ProducteevDashboard>) dashSelector.getAdapter();
adapter.insert(newDashboard, adapter.getCount()-1);
dashSelector.setSelection(adapter.getCount()-2);
refreshResponsibleSpinner(newDashboard.getUsers());
dialogUtilites.dismissDialog(context, progressDialog);
}
} catch (Exception e) {
dialogUtilites.dismissDialog(context, progressDialog);
dialogUtilites.okDialog(context,
context.getString(R.string.DLG_error, e.getMessage()),
new OnClickListener() {
@Override
public void onClick(DialogInterface theDialog, int theWhich) {
theDialog.dismiss();
}
});
e.printStackTrace();
dashSelector.setSelection(0);
}
}
}
};
OnClickListener cancelListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
dashboardSelector.setSelection(lastDashboardSelection);
}
};
dialogUtilites.viewDialog(ProducteevControlSet.this.activity,
ProducteevControlSet.this.activity.getString(R.string.producteev_create_dashboard_name),
editor,
okListener,
cancelListener);
} else {
refreshResponsibleSpinner(dashboard.getUsers()); refreshResponsibleSpinner(dashboard.getUsers());
lastDashboardSelection = position;
}
} }
@Override @Override
@ -140,8 +212,8 @@ public class ProducteevControlSet implements TaskEditControlSet {
ProducteevDashboard ownerDashboard = null; ProducteevDashboard ownerDashboard = null;
int dashboardSpinnerIndex = -1; int dashboardSpinnerIndex = -1;
//dashboard to not sync as first spinner-entry int i = 0;
for (int i=0;i<dashboardsData.length;i++) { for (i=0;i<dashboardsData.length;i++) {
ProducteevDashboard dashboard = new ProducteevDashboard(dashboardsData[i]); ProducteevDashboard dashboard = new ProducteevDashboard(dashboardsData[i]);
dashboards.add(dashboard); dashboards.add(dashboard);
if(dashboard.getId() == dashboardId) { if(dashboard.getId() == dashboardId) {
@ -149,7 +221,11 @@ public class ProducteevControlSet implements TaskEditControlSet {
dashboardSpinnerIndex = i; dashboardSpinnerIndex = i;
} }
} }
//dashboard to not sync as first spinner-entry
dashboards.add(0, new ProducteevDashboard(ProducteevUtilities.DASHBOARD_NO_SYNC, activity.getString(R.string.producteev_no_dashboard),null)); dashboards.add(0, new ProducteevDashboard(ProducteevUtilities.DASHBOARD_NO_SYNC, activity.getString(R.string.producteev_no_dashboard),null));
// dummyentry for adding a new dashboard
dashboards.add(new ProducteevDashboard(ProducteevUtilities.DASHBOARD_CREATE, activity.getString(R.string.producteev_create_dashboard),null));
ArrayAdapter<ProducteevDashboard> dashAdapter = new ArrayAdapter<ProducteevDashboard>(activity, ArrayAdapter<ProducteevDashboard> dashAdapter = new ArrayAdapter<ProducteevDashboard>(activity,
android.R.layout.simple_spinner_item, dashboards); android.R.layout.simple_spinner_item, dashboards);
@ -157,7 +233,8 @@ public class ProducteevControlSet implements TaskEditControlSet {
dashboardSelector.setAdapter(dashAdapter); dashboardSelector.setAdapter(dashAdapter);
dashboardSelector.setSelection(dashboardSpinnerIndex+1); dashboardSelector.setSelection(dashboardSpinnerIndex+1);
if (ownerDashboard == null || ownerDashboard.getId() == ProducteevUtilities.DASHBOARD_NO_SYNC) { if (ownerDashboard == null || ownerDashboard.getId() == ProducteevUtilities.DASHBOARD_NO_SYNC
|| ownerDashboard.getId() == ProducteevUtilities.DASHBOARD_CREATE) {
responsibleSelector.setEnabled(false); responsibleSelector.setEnabled(false);
responsibleSelector.setAdapter(null); responsibleSelector.setAdapter(null);
view.findViewById(R.id.producteev_TEA_task_assign_label).setVisibility(View.GONE); view.findViewById(R.id.producteev_TEA_task_assign_label).setVisibility(View.GONE);

@ -7,10 +7,10 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.astrid.adapter.TaskAdapter; import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.StoreObject; import com.todoroo.astrid.model.StoreObject;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard; import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
@ -26,7 +26,7 @@ import com.todoroo.astrid.utility.Preferences;
* @author Tim Su <tim@todoroo.com> * @author Tim Su <tim@todoroo.com>
* *
*/ */
public class ProducteevDetailExposer extends BroadcastReceiver implements DetailExposer{ public class ProducteevDetailExposer extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -51,7 +51,6 @@ public class ProducteevDetailExposer extends BroadcastReceiver implements Detail
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
} }
@Override
public String getTaskDetails(Context context, long id, boolean extended) { public String getTaskDetails(Context context, long id, boolean extended) {
Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(id); Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(id);
if(metadata == null) if(metadata == null)
@ -64,6 +63,9 @@ public class ProducteevDetailExposer extends BroadcastReceiver implements Detail
long responsibleId = -1; long responsibleId = -1;
if(metadata.containsNonNullValue(ProducteevTask.RESPONSIBLE_ID)) if(metadata.containsNonNullValue(ProducteevTask.RESPONSIBLE_ID))
responsibleId = metadata.getValue(ProducteevTask.RESPONSIBLE_ID); responsibleId = metadata.getValue(ProducteevTask.RESPONSIBLE_ID);
long creatorId = -1;
if(metadata.containsNonNullValue(ProducteevTask.CREATOR_ID))
creatorId = metadata.getValue(ProducteevTask.CREATOR_ID);
// display dashboard if not "no sync" or "default" // display dashboard if not "no sync" or "default"
StoreObject ownerDashboard = null; StoreObject ownerDashboard = null;
@ -86,14 +88,21 @@ public class ProducteevDetailExposer extends BroadcastReceiver implements Detail
// display responsible user if not current one // display responsible user if not current one
if(responsibleId > 0 && ownerDashboard != null && responsibleId != if(responsibleId > 0 && ownerDashboard != null && responsibleId !=
Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L)) { Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L)) {
String users = ";" + ownerDashboard.getValue(ProducteevDashboard.USERS); //$NON-NLS-1$ String user = getUserFromDashboard(ownerDashboard, responsibleId);
int index = users.indexOf(";" + responsibleId + ","); //$NON-NLS-1$ //$NON-NLS-2$ if(user != null)
if(index > -1) {
String user = users.substring(users.indexOf(',', index) + 1,
users.indexOf(';', index + 1));
builder.append("<img src='silk_user_gray'/> ").append(user).append(TaskAdapter.DETAIL_SEPARATOR); //$NON-NLS-1$ builder.append("<img src='silk_user_gray'/> ").append(user).append(TaskAdapter.DETAIL_SEPARATOR); //$NON-NLS-1$
} }
// display creator user if not the current one
if(creatorId > 0 && ownerDashboard != null && creatorId !=
Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L)) {
String user = getUserFromDashboard(ownerDashboard, creatorId);
if(user != null)
builder.append("<img src='silk_user_orange'/> ").append( //$NON-NLS-1$
context.getString(R.string.producteev_PDE_task_from, user)).
append(TaskAdapter.DETAIL_SEPARATOR);
} }
} else { } else {
TodorooCursor<Metadata> notesCursor = ProducteevDataService.getInstance().getTaskNotesCursor(id); TodorooCursor<Metadata> notesCursor = ProducteevDataService.getInstance().getTaskNotesCursor(id);
try { try {
@ -112,9 +121,14 @@ public class ProducteevDetailExposer extends BroadcastReceiver implements Detail
return result.substring(0, result.length() - TaskAdapter.DETAIL_SEPARATOR.length()); return result.substring(0, result.length() - TaskAdapter.DETAIL_SEPARATOR.length());
} }
@Override /** Try and find user in the dashboard. return null if un-findable */
public String getPluginIdentifier() { private String getUserFromDashboard(StoreObject dashboard, long userId) {
return ProducteevUtilities.IDENTIFIER; String users = ";" + dashboard.getValue(ProducteevDashboard.USERS); //$NON-NLS-1$
int index = users.indexOf(";" + userId + ","); //$NON-NLS-1$ //$NON-NLS-2$
if(index > -1)
return users.substring(users.indexOf(',', index) + 1,
users.indexOf(';', index + 1));
return null;
} }
} }

@ -3,8 +3,8 @@
*/ */
package com.todoroo.astrid.producteev; package com.todoroo.astrid.producteev;
import java.util.Map.Entry;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.Map.Entry;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentValues; import android.content.ContentValues;
@ -39,7 +39,7 @@ public class ProducteevFilterExposer extends BroadcastReceiver {
/** /**
* @param context * @param context
*/ */
private Filter filterFromList(Context context, ProducteevDashboard dashboard) { public static Filter filterFromList(Context context, ProducteevDashboard dashboard) {
String dashboardTitle = dashboard.getName(); String dashboardTitle = dashboard.getName();
String title = dashboard.getName(); String title = dashboard.getName();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();

@ -111,21 +111,15 @@ public class ProducteevLoginActivity extends Activity {
else { else {
Editable email = emailEditText.getText(); Editable email = emailEditText.getText();
Editable password = passwordEditText.getText(); Editable password = passwordEditText.getText();
Editable confirmPassword = ((EditText)findViewById(R.id.confirmPassword)).getText();
Editable firstName = ((EditText)findViewById(R.id.firstName)).getText(); Editable firstName = ((EditText)findViewById(R.id.firstName)).getText();
Editable lastName = ((EditText)findViewById(R.id.lastName)).getText(); Editable lastName = ((EditText)findViewById(R.id.lastName)).getText();
if(email.length() == 0 || password.length() == 0 || if(email.length() == 0 || password.length() == 0 ||
confirmPassword.length() == 0 || firstName.length() == 0 || firstName.length() == 0 ||
lastName.length() == 0) { lastName.length() == 0) {
errors.setVisibility(View.VISIBLE); errors.setVisibility(View.VISIBLE);
errors.setText(R.string.producteev_PLA_errorEmpty); errors.setText(R.string.producteev_PLA_errorEmpty);
return; return;
} }
if(!confirmPassword.toString().equals(password.toString())) {
errors.setVisibility(View.VISIBLE);
errors.setText(R.string.producteev_PLA_errorMatch);
return;
}
performSignup(email.toString(), password.toString(), performSignup(email.toString(), password.toString(),
firstName.toString(), lastName.toString()); firstName.toString(), lastName.toString());
} }

@ -1,6 +1,5 @@
package com.todoroo.astrid.producteev; package com.todoroo.astrid.producteev;
import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.preference.ListPreference; import android.preference.ListPreference;
@ -31,8 +30,7 @@ public class ProducteevPreferences extends SyncProviderPreferences {
@Override @Override
public void startSync() { public void startSync() {
startService(new Intent(ProducteevBackgroundService.SYNC_ACTION, null, new ProducteevSyncProvider().synchronize(this);
this, ProducteevBackgroundService.class));
} }
@Override @Override

@ -0,0 +1,39 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.SyncAction;
/**
* Exposes sync action
*
*/
public class ProducteevSyncActionExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose sync action
if(!ProducteevUtilities.INSTANCE.isLoggedIn())
return;
Intent syncIntent = new Intent(ProducteevBackgroundService.SYNC_ACTION, null,
context, ProducteevBackgroundService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, syncIntent, PendingIntent.FLAG_UPDATE_CURRENT);
SyncAction syncAction = new SyncAction(context.getString(R.string.producteev_PPr_header),
pendingIntent);
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, syncAction);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -18,6 +18,9 @@ public class ProducteevUtilities extends SyncProviderUtilities {
public static final ProducteevUtilities INSTANCE = new ProducteevUtilities(); public static final ProducteevUtilities INSTANCE = new ProducteevUtilities();
/** setting for dashboard to getting created */
public static final int DASHBOARD_CREATE = -2;
/** setting for dashboard to not synchronize */ /** setting for dashboard to not synchronize */
public static final int DASHBOARD_NO_SYNC = -1; public static final int DASHBOARD_NO_SYNC = -1;
@ -38,6 +41,10 @@ public class ProducteevUtilities extends SyncProviderUtilities {
public static final String PREF_SERVER_LAST_SYNC = IDENTIFIER + "_last_server"; //$NON-NLS-1$ public static final String PREF_SERVER_LAST_SYNC = IDENTIFIER + "_last_server"; //$NON-NLS-1$
public static final String PREF_SERVER_LAST_NOTIFICATION = IDENTIFIER + "_last_notification"; //$NON-NLS-1$
public static final String PREF_SERVER_LAST_ACTIVITY = IDENTIFIER + "_last_activity"; //$NON-NLS-1$
/** Producteev user's default dashboard. This is different from the /** Producteev user's default dashboard. This is different from the
* preference key, which indicates where user wants to put new tasks */ * preference key, which indicates where user wants to put new tasks */
public static final String PREF_DEFAULT_DASHBOARD = IDENTIFIER + "_defaultdash"; //$NON-NLS-1$ public static final String PREF_DEFAULT_DASHBOARD = IDENTIFIER + "_defaultdash"; //$NON-NLS-1$

@ -111,6 +111,31 @@ public class ProducteevInvoker {
"since", since), "dashboards"); "since", since), "dashboards");
} }
/**
* create a dasbhoard
*
* @param name
* @return the new created dashboard as JSONObject
*/
public JSONObject dashboardsCreate(String name) throws ApiServiceException, IOException {
return callAuthenticated("dashboards/create.json",
"token", token,
"title", name);
}
/**
* return the list of users who can access a specific dashboard
*
* @param idDashboard
* @param dashboard array-information about the dashboard, if this ...
*/
public JSONArray dashboardsAccess(long idDashboard, String dashboard) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("dashboards/access.json",
"token", token,
"id_dashboard", idDashboard,
"dashboard", dashboard),"dashboard");
}
// --- tasks // --- tasks
/** /**
@ -217,14 +242,16 @@ public class ProducteevInvoker {
* *
* @param idTask * @param idTask
* @param deadline * @param deadline
* @param allDay (optional), 1: all day task (time specified in deadline will be ignored), 0: deadline with time
* *
* @return array tasks/view * @return array tasks/view
*/ */
public JSONObject tasksSetDeadline(long idTask, String deadline) throws ApiServiceException, IOException { public JSONObject tasksSetDeadline(long idTask, String deadline, Integer allDay) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_deadline.json", return callAuthenticated("tasks/set_deadline.json",
"token", token, "token", token,
"id_task", idTask, "id_task", idTask,
"deadline", deadline); "deadline", deadline,
"all_day", allDay);
} }
/** /**
@ -374,6 +401,36 @@ public class ProducteevInvoker {
"title", title); "title", title);
} }
// --- notifications/activities
/**
* get every activities
*
* @param dashboardId (optional) if not null, this function only returns notifications for this specific dashboard
* @param lastId (optional) this function returns only activities later than this id
*/
public JSONArray activitiesShowActivities(Long dashboardId, Long lastId) throws ApiResponseParseException, ApiServiceException, IOException {
return getResponse(callAuthenticated("activities/show_activities.json",
"token", token,
"id_dashboard", dashboardId,
"last_id", lastId), "activities");
}
/**
* get every notification for the current user
* @param dashboardId
* @param lastId
* @return
* @throws ApiResponseParseException
* @throws ApiServiceException
* @throws IOException
*/
public JSONArray activitiesShowNotifications(Long dashboardId, Long lastId) throws ApiResponseParseException, ApiServiceException, IOException {
return getResponse(callAuthenticated("activities/show_notifications.json",
"token", token,
"id_dashboard", dashboardId,
"last_id", lastId), "activities");
}
// --- users // --- users
/** /**
@ -389,19 +446,6 @@ public class ProducteevInvoker {
"id_colleague", idColleague); "id_colleague", idColleague);
} }
/**
* return the list of users who can access a specific dashboard
*
* @param idDashboard
* @param dashboard array-information about the dashboard, if this ...
*/
public JSONArray dashboardsAccess(long idDashboard, String dashboard) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("dashboards/access",
"token", token,
"id_dashboard", idDashboard,
"dashboard", dashboard),"dashboard");
}
// --- invocation // --- invocation
private final ProducteevRestClient restClient = new ProducteevRestClient(); private final ProducteevRestClient restClient = new ProducteevRestClient();

@ -38,7 +38,7 @@ public class ProducteevDashboard {
private ArrayList<ProducteevUser> users = null; private ArrayList<ProducteevUser> users = null;
public ProducteevDashboard (StoreObject dashboardData) { public ProducteevDashboard (StoreObject dashboardData) {
this(dashboardData.getValue(REMOTE_ID),dashboardData.getValue(NAME),dashboardData.getValue(USERS)); this(dashboardData.getValue(REMOTE_ID),dashboardData.getValue(NAME),(dashboardData.containsValue(USERS)?dashboardData.getValue(USERS):null));
} }
/** /**

@ -20,15 +20,17 @@ import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join; import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.StoreObjectDao; import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.StoreObject; import com.todoroo.astrid.model.StoreObject;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.producteev.ProducteevUtilities; import com.todoroo.astrid.producteev.ProducteevUtilities;
import com.todoroo.astrid.producteev.api.ApiUtilities;
import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TagService;
@ -82,6 +84,7 @@ public final class ProducteevDataService {
metadataService.deleteWhere(Metadata.KEY.eq(ProducteevTask.METADATA_KEY)); metadataService.deleteWhere(Metadata.KEY.eq(ProducteevTask.METADATA_KEY));
metadataService.deleteWhere(Metadata.KEY.eq(ProducteevNote.METADATA_KEY)); metadataService.deleteWhere(Metadata.KEY.eq(ProducteevNote.METADATA_KEY));
storeObjectDao.deleteWhere(StoreObject.TYPE.eq(ProducteevDashboard.TYPE)); storeObjectDao.deleteWhere(StoreObject.TYPE.eq(ProducteevDashboard.TYPE));
PluginServices.getTaskService().clearDetails();
} }
/** /**
@ -244,6 +247,17 @@ public final class ProducteevDataService {
readDashboards(); readDashboards();
for(int i = 0; i < changedDashboards.length(); i++) { for(int i = 0; i < changedDashboards.length(); i++) {
JSONObject remote = changedDashboards.getJSONObject(i).getJSONObject("dashboard"); JSONObject remote = changedDashboards.getJSONObject(i).getJSONObject("dashboard");
updateDashboards(remote, false);
}
// clear dashboard cache
dashboards = null;
}
@SuppressWarnings("nls")
public StoreObject updateDashboards(JSONObject remote, boolean reinitCache) throws JSONException {
if (reinitCache)
readDashboards();
long id = remote.getLong("id_dashboard"); long id = remote.getLong("id_dashboard");
StoreObject local = null; StoreObject local = null;
for(StoreObject dashboard : dashboards) { for(StoreObject dashboard : dashboards) {
@ -256,29 +270,27 @@ public final class ProducteevDataService {
if(remote.getInt("deleted") != 0) { if(remote.getInt("deleted") != 0) {
if(local != null) if(local != null)
storeObjectDao.delete(local.getId()); storeObjectDao.delete(local.getId());
continue;
} }
if(local == null) if(local == null)
local = new StoreObject(); local = new StoreObject();
local.setValue(StoreObject.TYPE, ProducteevDashboard.TYPE); local.setValue(StoreObject.TYPE, ProducteevDashboard.TYPE);
local.setValue(ProducteevDashboard.REMOTE_ID, id); local.setValue(ProducteevDashboard.REMOTE_ID, id);
local.setValue(ProducteevDashboard.NAME, remote.getString("title")); local.setValue(ProducteevDashboard.NAME, ApiUtilities.decode(remote.getString("title")));
StringBuilder users = new StringBuilder(); StringBuilder users = new StringBuilder();
JSONArray accessList = remote.getJSONArray("accesslist"); JSONArray accessList = remote.getJSONArray("accesslist");
for(int j = 0; j < accessList.length(); j++) { for(int j = 0; j < accessList.length(); j++) {
JSONObject user = accessList.getJSONObject(j).getJSONObject("user"); JSONObject user = accessList.getJSONObject(j).getJSONObject("user");
users.append(user.getLong("id_user")).append(','); users.append(user.getLong("id_user")).append(',');
String name = user.optString("firstname", "") + ' ' + String name = ApiUtilities.decode(user.optString("firstname", "") + ' ' +
user.optString("lastname", ""); user.optString("lastname", ""));
users.append(name.trim()).append(';'); users.append(name.trim()).append(';');
} }
local.setValue(ProducteevDashboard.USERS, users.toString()); local.setValue(ProducteevDashboard.USERS, users.toString());
storeObjectDao.persist(local); storeObjectDao.persist(local);
} if (reinitCache)
// clear dashboard cache
dashboards = null; dashboards = null;
return local;
} }
} }

@ -15,6 +15,7 @@ import org.json.JSONObject;
import android.app.Activity; import android.app.Activity;
import android.app.Notification; import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.text.TextUtils; import android.text.TextUtils;
@ -27,15 +28,20 @@ import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.activity.ShortcutActivity;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.TaskContainer; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.common.SyncProvider; import com.todoroo.astrid.common.SyncProvider;
import com.todoroo.astrid.common.TaskContainer;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.StoreObject; import com.todoroo.astrid.model.StoreObject;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.producteev.ProducteevBackgroundService;
import com.todoroo.astrid.producteev.ProducteevFilterExposer;
import com.todoroo.astrid.producteev.ProducteevLoginActivity; import com.todoroo.astrid.producteev.ProducteevLoginActivity;
import com.todoroo.astrid.producteev.ProducteevPreferences; import com.todoroo.astrid.producteev.ProducteevPreferences;
import com.todoroo.astrid.producteev.ProducteevUtilities; import com.todoroo.astrid.producteev.ProducteevUtilities;
@ -45,6 +51,7 @@ import com.todoroo.astrid.producteev.api.ApiUtilities;
import com.todoroo.astrid.producteev.api.ProducteevInvoker; import com.todoroo.astrid.producteev.api.ProducteevInvoker;
import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Preferences; import com.todoroo.astrid.utility.Preferences;
@SuppressWarnings("nls") @SuppressWarnings("nls")
@ -74,7 +81,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// ------------------------------------------------------- public methods // ------------------------------------------------------ utility methods
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
/** /**
@ -84,16 +91,15 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
preferences.setToken(null); preferences.setToken(null);
Preferences.setString(R.string.producteev_PPr_email, null); Preferences.setString(R.string.producteev_PPr_email, null);
Preferences.setString(R.string.producteev_PPr_password, null); Preferences.setString(R.string.producteev_PPr_password, null);
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_SYNC, null);
Preferences.setStringFromInteger(R.string.producteev_PPr_defaultdash_key,
ProducteevUtilities.DASHBOARD_DEFAULT);
preferences.clearLastSyncDate(); preferences.clearLastSyncDate();
dataService = ProducteevDataService.getInstance(); dataService = ProducteevDataService.getInstance();
dataService.clearMetadata(); dataService.clearMetadata();
} }
// ----------------------------------------------------------------------
// ------------------------------------------------------- authentication
// ----------------------------------------------------------------------
/** /**
* Deal with a synchronization exception. If requested, will show an error * Deal with a synchronization exception. If requested, will show an error
* to the user (unless synchronization is happening in background) * to the user (unless synchronization is happening in background)
@ -107,44 +113,40 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
* whether to display a dialog * whether to display a dialog
*/ */
@Override @Override
protected void handleException(String tag, Exception e, boolean showError) { protected void handleException(String tag, Exception e, boolean displayError) {
final Context context = ContextManager.getContext();
preferences.setLastError(e.toString()); preferences.setLastError(e.toString());
String message = null;
// occurs when application was closed // occurs when application was closed
if(e instanceof IllegalStateException) { if(e instanceof IllegalStateException) {
exceptionService.reportError(tag + "-caught", e); //$NON-NLS-1$ exceptionService.reportError(tag + "-caught", e); //$NON-NLS-1$
// occurs when network error // occurs when network error
} else if(!(e instanceof ApiServiceException) && e instanceof IOException) { } else if(!(e instanceof ApiServiceException) && e instanceof IOException) {
exceptionService.reportError(tag + "-ioexception", e); //$NON-NLS-1$ message = context.getString(R.string.producteev_ioerror);
if(showError) {
Context context = ContextManager.getContext();
showError(context, e, context.getString(R.string.producteev_ioerror));
}
} else { } else {
message = context.getString(R.string.DLG_error, e.toString());
exceptionService.reportError(tag + "-unhandled", e); //$NON-NLS-1$ exceptionService.reportError(tag + "-unhandled", e); //$NON-NLS-1$
if(showError) {
Context context = ContextManager.getContext();
showError(context, e, null);
} }
if(displayError && context instanceof Activity && message != null) {
dialogUtilities.okDialog((Activity)context,
message, null);
} }
} }
@Override // ----------------------------------------------------------------------
protected void initiate(Context context) { // ------------------------------------------------------ initiating sync
dataService = ProducteevDataService.getInstance(); // ----------------------------------------------------------------------
// authenticate the user. this will automatically call the next step
authenticate();
}
/** /**
* Perform authentication with RTM. Will open the SyncBrowser if necessary * initiate sync in background
*/ */
private void authenticate() { @Override
FlurryAgent.onEvent("producteev-started"); protected void initiateBackground(Service service) {
dataService = ProducteevDataService.getInstance();
preferences.recordSyncStart();
try { try {
String authToken = preferences.getToken(); String authToken = preferences.getToken();
@ -159,16 +161,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
performSync(); performSync();
} else { } else {
if (email == null && password == null) { if (email == null && password == null) {
// display login-activity // we can't do anything, user is not logged in
final Context context = ContextManager.getContext();
Intent intent = new Intent(context, ProducteevLoginActivity.class);
if(context instanceof Activity)
((Activity)context).startActivityForResult(intent, 0);
else {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
ProducteevUtilities.INSTANCE.stopOngoing();
}
} else { } else {
invoker.authenticate(email, password); invoker.authenticate(email, password);
preferences.setToken(invoker.getToken()); preferences.setToken(invoker.getToken());
@ -184,6 +177,25 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
} }
} }
/**
* If user isn't already signed in, show sign in dialog. Else perform sync.
*/
@Override
protected void initiateManual(Activity activity) {
String authToken = preferences.getToken();
ProducteevUtilities.INSTANCE.stopOngoing();
// check if we have a token & it works
if(authToken == null) {
// display login-activity
Intent intent = new Intent(activity, ProducteevLoginActivity.class);
activity.startActivityForResult(intent, 0);
} else {
activity.startService(new Intent(ProducteevBackgroundService.SYNC_ACTION, null,
activity, ProducteevBackgroundService.class));
}
}
public static ProducteevInvoker getInvoker() { public static ProducteevInvoker getInvoker() {
String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2"); String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2");
String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae"); String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae");
@ -195,6 +207,9 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
protected void performSync() { protected void performSync() {
FlurryAgent.onEvent("producteev-started");
preferences.recordSyncStart();
try { try {
// load user information // load user information
JSONObject user = invoker.usersView(null).getJSONObject("user"); JSONObject user = invoker.usersView(null).getJSONObject("user");
@ -202,9 +217,11 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
long userId = user.getLong("id_user"); long userId = user.getLong("id_user");
String lastServerSync = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_SYNC); String lastServerSync = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_SYNC);
String lastNotificationId = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_NOTIFICATION);
String lastActivityId = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_ACTIVITY);
// read dashboards // read dashboards
JSONArray dashboards = invoker.dashboardsShowList(lastServerSync); JSONArray dashboards = invoker.dashboardsShowList(null);
dataService.updateDashboards(dashboards); dataService.updateDashboards(dashboards);
// read labels and tasks for each dashboard // read labels and tasks for each dashboard
@ -217,6 +234,9 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
JSONArray tasks = invoker.tasksShowList(dashboardId, lastServerSync); JSONArray tasks = invoker.tasksShowList(dashboardId, lastServerSync);
for(int i = 0; i < tasks.length(); i++) { for(int i = 0; i < tasks.length(); i++) {
ProducteevTaskContainer remote = parseRemoteTask(tasks.getJSONObject(i)); ProducteevTaskContainer remote = parseRemoteTask(tasks.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);
boolean foundLocal = dataService.findLocalMatch(remote); boolean foundLocal = dataService.findLocalMatch(remote);
// if creator & responsible != current user, skip / delete it // if creator & responsible != current user, skip / delete it
@ -246,6 +266,56 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH); Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
// notification/activities stuff
JSONArray notifications = invoker.activitiesShowNotifications(null, (lastNotificationId == null ? null : new Long(lastNotificationId)));
String[] notificationsList = parseActivities(notifications);
// update lastIds
if (notifications.length() > 0) {
lastNotificationId = ""+notifications.getJSONObject(0).getJSONObject("activity").getLong("id_activity");
}
// display notifications from producteev-log
Context context = ContextManager.getContext();
final NotificationManager nm = new NotificationManager.AndroidNotificationManager(context);
for (int i = 0; i< notificationsList.length; i++) {
long id_dashboard = notifications.getJSONObject(i).getJSONObject("activity").getLong("id_dashboard");
String dashboardName = null;
StoreObject[] dashboardsData = ProducteevDataService.getInstance().getDashboards();
ProducteevDashboard dashboard = null;
if (dashboardsData != null) {
for (int j=0; i<dashboardsData.length;i++) {
long id = dashboardsData[j].getValue(ProducteevDashboard.REMOTE_ID);
if (id == id_dashboard) {
dashboardName = dashboardsData[j].getValue(ProducteevDashboard.NAME);
dashboard = new ProducteevDashboard(id, dashboardName, null);
break;
}
}
}
// it seems dashboard is null if we get a notification about an unknown dashboard, just filter it.
if (dashboard != null) {
// initialize notification
int icon = R.drawable.ic_producteev_notification;
long when = System.currentTimeMillis();
Notification notification = new Notification(icon, null, when);
CharSequence contentTitle = context.getString(R.string.producteev_notification_title)+": "+dashboard.getName();
Filter filter = ProducteevFilterExposer.filterFromList(context, dashboard);
Intent notificationIntent = ShortcutActivity.createIntent(filter);
// filter the tags from the message
String message = notificationsList[i].replaceAll("<[^>]+>", "");
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
notification.setLatestEventInfo(context, contentTitle, message, contentIntent);
nm.notify(Constants.NOTIFICATION_PRODUCTEEV_NOTIFICATIONS-i, notification);
}
}
// store lastIds in Preferences
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_NOTIFICATION, lastNotificationId);
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_ACTIVITY, lastActivityId);
FlurryAgent.onEvent("pdv-sync-finished"); //$NON-NLS-1$ FlurryAgent.onEvent("pdv-sync-finished"); //$NON-NLS-1$
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// occurs when application was closed // occurs when application was closed
@ -254,6 +324,24 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
} }
} }
/**
* @param activities
* @return
* @throws JSONException
*/
private String[] parseActivities(JSONArray activities) throws JSONException {
int count = (activities == null ? 0 : activities.length());
String[] activitiesList = new String[count];
if(activities == null)
return activitiesList;
for(int i = 0; i < activities.length(); i++) {
String message = activities.getJSONObject(i).getJSONObject("activity").getString("message");
activitiesList[i] = ApiUtilities.decode(message);
}
return activitiesList;
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// ------------------------------------------------------------ sync data // ------------------------------------------------------------ sync data
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@ -263,6 +351,12 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
long userId = user.getLong("id_user"); long userId = user.getLong("id_user");
Preferences.setLong(ProducteevUtilities.PREF_DEFAULT_DASHBOARD, defaultDashboard); Preferences.setLong(ProducteevUtilities.PREF_DEFAULT_DASHBOARD, defaultDashboard);
Preferences.setLong(ProducteevUtilities.PREF_USER_ID, userId); Preferences.setLong(ProducteevUtilities.PREF_USER_ID, userId);
// save the default dashboard preference if unset
int defaultDashSetting = Preferences.getIntegerFromString(R.string.producteev_PPr_defaultdash_key,
ProducteevUtilities.DASHBOARD_DEFAULT);
if(defaultDashSetting == ProducteevUtilities.DASHBOARD_DEFAULT)
Preferences.setStringFromInteger(R.string.producteev_PPr_defaultdash_key, (int) defaultDashboard);
} }
// all synchronized properties // all synchronized properties
@ -302,6 +396,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
long dashboard = ProducteevUtilities.INSTANCE.getDefaultDashboard(); long dashboard = ProducteevUtilities.INSTANCE.getDefaultDashboard();
if(local.pdvTask.containsNonNullValue(ProducteevTask.DASHBOARD_ID)) if(local.pdvTask.containsNonNullValue(ProducteevTask.DASHBOARD_ID))
dashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID); dashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID);
long responsibleId = local.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID);
if(dashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) { if(dashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) {
// set a bogus task id, then return without creating // set a bogus task id, then return without creating
@ -310,7 +405,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
} }
JSONObject response = invoker.tasksCreate(localTask.getValue(Task.TITLE), JSONObject response = invoker.tasksCreate(localTask.getValue(Task.TITLE),
null, dashboard, createDeadline(localTask), createReminder(localTask), responsibleId, dashboard, createDeadline(localTask), createReminder(localTask),
localTask.isCompleted() ? 2 : 1, createStars(localTask)); localTask.isCompleted() ? 2 : 1, createStars(localTask));
ProducteevTaskContainer newRemoteTask; ProducteevTaskContainer newRemoteTask;
try { try {
@ -338,6 +433,9 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
task.setValue(Task.DELETION_DATE, remoteTask.getInt("deleted") == 1 ? DateUtilities.now() : 0); task.setValue(Task.DELETION_DATE, remoteTask.getInt("deleted") == 1 ? DateUtilities.now() : 0);
long dueDate = ApiUtilities.producteevToUnixTime(remoteTask.getString("deadline"), 0); long dueDate = ApiUtilities.producteevToUnixTime(remoteTask.getString("deadline"), 0);
if(remoteTask.optInt("all_day", 0) == 1)
task.setValue(Task.DUE_DATE, task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate));
else
task.setValue(Task.DUE_DATE, task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueDate)); task.setValue(Task.DUE_DATE, task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueDate));
task.setValue(Task.IMPORTANCE, 5 - remoteTask.getInt("star")); task.setValue(Task.IMPORTANCE, 5 - remoteTask.getInt("star"));
@ -386,6 +484,7 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
protected void push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException { protected void push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException {
long idTask = local.pdvTask.getValue(ProducteevTask.ID); long idTask = local.pdvTask.getValue(ProducteevTask.ID);
long idDashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID); long idDashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID);
long idResponsible = local.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID);
// if local is marked do not sync, handle accordingly // if local is marked do not sync, handle accordingly
if(idDashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) { if(idDashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) {
@ -420,13 +519,19 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
remote = create(local); remote = create(local);
} }
// responsible
if(remote != null && idResponsible !=
remote.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID)) {
invoker.tasksSetResponsible(idTask, idResponsible);
}
// core properties // core properties
if(shouldTransmit(local, Task.TITLE, remote)) if(shouldTransmit(local, Task.TITLE, remote))
invoker.tasksSetTitle(idTask, local.task.getValue(Task.TITLE)); invoker.tasksSetTitle(idTask, local.task.getValue(Task.TITLE));
if(shouldTransmit(local, Task.IMPORTANCE, remote)) if(shouldTransmit(local, Task.IMPORTANCE, remote))
invoker.tasksSetStar(idTask, createStars(local.task)); invoker.tasksSetStar(idTask, createStars(local.task));
if(shouldTransmit(local, Task.DUE_DATE, remote)) if(shouldTransmit(local, Task.DUE_DATE, remote) && local.task.hasDueDate()) // temporary can't unset deadline
invoker.tasksSetDeadline(idTask, createDeadline(local.task)); invoker.tasksSetDeadline(idTask, createDeadline(local.task), local.task.hasDueTime() ? 0 : 1);
if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) if(shouldTransmit(local, Task.COMPLETION_DATE, remote))
invoker.tasksSetStatus(idTask, local.task.isCompleted() ? 2 : 1); invoker.tasksSetStatus(idTask, local.task.isCompleted() ? 2 : 1);
@ -575,11 +680,8 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
*/ */
private String createDeadline(Task task) { private String createDeadline(Task task) {
if(!task.hasDueDate()) if(!task.hasDueDate())
return null; return "";
if(!task.hasDueTime()) return ApiUtilities.unixTimeToProducteev(task.getValue(Task.DUE_DATE));
return ApiUtilities.unixDateToProducteev(task.getValue(Task.DUE_DATE));
String time = ApiUtilities.unixTimeToProducteev(task.getValue(Task.DUE_DATE));
return time.substring(0, time.lastIndexOf(' '));
} }
/** /**

@ -5,7 +5,7 @@ import java.util.Iterator;
import org.json.JSONObject; import org.json.JSONObject;
import com.todoroo.astrid.api.TaskContainer; import com.todoroo.astrid.common.TaskContainer;
import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;

@ -60,7 +60,6 @@ public class NotificationActivity extends TaskListActivity implements OnTimeSetL
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
populateFilter(getIntent());
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
displayNotificationPopup(); displayNotificationPopup();

@ -11,6 +11,7 @@ import android.content.res.Resources;
import android.graphics.Color; import android.graphics.Color;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import com.timsu.astrid.R; import com.timsu.astrid.R;
@ -111,7 +112,7 @@ public class Notifications extends BroadcastReceiver {
public boolean showTaskNotification(long id, int type, String reminder) { public boolean showTaskNotification(long id, int type, String reminder) {
Task task; Task task;
try { try {
task = taskDao.fetch(id, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE, task = taskDao.fetch(id, Task.ID, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE,
Task.DELETION_DATE, Task.REMINDER_FLAGS); Task.DELETION_DATE, Task.REMINDER_FLAGS);
if(task == null) if(task == null)
throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$ throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$
@ -121,9 +122,6 @@ public class Notifications extends BroadcastReceiver {
return false; return false;
} }
// schedule next notification
ReminderService.getInstance().scheduleAlarm(task);
// you're done - don't sound, do delete // you're done - don't sound, do delete
if(task.isCompleted() || task.isDeleted()) if(task.isCompleted() || task.isDeleted())
return false; return false;
@ -138,7 +136,11 @@ public class Notifications extends BroadcastReceiver {
// update last reminder time // update last reminder time
task.setValue(Task.REMINDER_LAST, DateUtilities.now()); task.setValue(Task.REMINDER_LAST, DateUtilities.now());
taskDao.saveExisting(task); boolean saved = taskDao.saveExisting(task);
// schedule next notification (unless couldn't save last time)
if(saved)
ReminderService.getInstance().scheduleAlarm(task);
Context context = ContextManager.getContext(); Context context = ContextManager.getContext();
String title = context.getString(R.string.app_name); String title = context.getString(R.string.app_name);
@ -146,7 +148,7 @@ public class Notifications extends BroadcastReceiver {
Intent notifyIntent = new Intent(context, NotificationActivity.class); Intent notifyIntent = new Intent(context, NotificationActivity.class);
notifyIntent.putExtra(NotificationActivity.TOKEN_ID, id); notifyIntent.putExtra(NotificationActivity.TOKEN_ID, id);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
showNotification((int)id, notifyIntent, type, title, text, nonstopMode); showNotification((int)id, notifyIntent, type, title, text, nonstopMode);
return true; return true;
@ -215,6 +217,10 @@ public class Notifications extends BroadcastReceiver {
AudioManager audioManager = (AudioManager)context.getSystemService( AudioManager audioManager = (AudioManager)context.getSystemService(
Context.AUDIO_SERVICE); Context.AUDIO_SERVICE);
// detect call state
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
int callState = tm.getCallState();
// if non-stop mode is activated, set up the flags for insistent // if non-stop mode is activated, set up the flags for insistent
// notification, and increase the volume to full volume, so the user // notification, and increase the volume to full volume, so the user
// will actually pay attention to the alarm // will actually pay attention to the alarm
@ -228,7 +234,7 @@ public class Notifications extends BroadcastReceiver {
} }
// quiet hours = no sound // quiet hours = no sound
if(quietHours) { if(quietHours || callState != TelephonyManager.CALL_STATE_IDLE) {
notification.sound = null; notification.sound = null;
} else { } else {
String notificationPreference = Preferences.getStringValue(R.string.p_rmd_ringtone); String notificationPreference = Preferences.getStringValue(R.string.p_rmd_ringtone);
@ -247,8 +253,9 @@ public class Notifications extends BroadcastReceiver {
} }
// quiet hours && ! due date or snooze = no vibrate // quiet hours && ! due date or snooze = no vibrate
if(quietHours && !(type == ReminderService.TYPE_DUE || if(quietHours && !(type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_SNOOZE)) {
type == ReminderService.TYPE_SNOOZE)) { notification.vibrate = null;
} else if(callState != TelephonyManager.CALL_STATE_IDLE) {
notification.vibrate = null; notification.vibrate = null;
} else { } else {
if (Preferences.getBoolean(R.string.p_rmd_vibrate, true) if (Preferences.getBoolean(R.string.p_rmd_vibrate, true)

@ -39,6 +39,7 @@ public final class ReminderService {
private static final Property<?>[] PROPERTIES = new Property<?>[] { private static final Property<?>[] PROPERTIES = new Property<?>[] {
Task.ID, Task.ID,
Task.CREATION_DATE,
Task.COMPLETION_DATE, Task.COMPLETION_DATE,
Task.DELETION_DATE, Task.DELETION_DATE,
Task.DUE_DATE, Task.DUE_DATE,
@ -186,8 +187,11 @@ public final class ReminderService {
} }
/** /**
* Calculate the next alarm time for overdue reminders. If the due date * Calculate the next alarm time for overdue reminders.
* has passed, we schedule a reminder some time in the next 4 - 24 hours. * <p>
* We schedule an alarm for after the due date (which could be in the past),
* with the exception that if a reminder was recently issued, we move
* the alarm time to the near future.
* *
* @param task * @param task
* @return * @return
@ -195,17 +199,31 @@ public final class ReminderService {
private long calculateNextOverdueReminder(Task task) { private long calculateNextOverdueReminder(Task task) {
if(task.hasDueDate() && task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE)) { if(task.hasDueDate() && task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE)) {
long dueDate = task.getValue(Task.DUE_DATE); long dueDate = task.getValue(Task.DUE_DATE);
if(dueDate < DateUtilities.now()) long lastReminder = task.getValue(Task.REMINDER_LAST);
dueDate = DateUtilities.now();
return dueDate + (long)((4 + 30 * random.nextFloat()) * DateUtilities.ONE_HOUR); if(dueDate > DateUtilities.now())
return dueDate + (long)((0.5f + 2f * random.nextFloat()) * DateUtilities.ONE_HOUR);
if(lastReminder < dueDate)
return DateUtilities.now();
if(DateUtilities.now() - lastReminder < 6 * DateUtilities.ONE_HOUR)
return DateUtilities.now() + (long)((1.0f + 3f * random.nextFloat()) * DateUtilities.ONE_HOUR);
return DateUtilities.now();
} }
return NO_ALARM; return NO_ALARM;
} }
/** /**
* Calculate the next alarm time for due date reminders. If the due date * Calculate the next alarm time for due date reminders.
* has not already passed, we return the due date, altering the time * <p>
* if the date was indicated to not have a due time * This alarm always returns the due date, and is triggered if
* the last reminder time occurred before the due date. This means it is
* possible to return due dates in the past.
* <p>
* If the date was indicated to not have a due time, we read from
* preferences and assign a time.
* *
* @param task * @param task
* @return * @return
@ -213,26 +231,38 @@ public final class ReminderService {
private long calculateNextDueDateReminder(Task task) { private long calculateNextDueDateReminder(Task task) {
if(task.hasDueDate() && task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AT_DEADLINE)) { if(task.hasDueDate() && task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AT_DEADLINE)) {
long dueDate = task.getValue(Task.DUE_DATE); long dueDate = task.getValue(Task.DUE_DATE);
if(dueDate < DateUtilities.now()) long lastReminder = task.getValue(Task.REMINDER_LAST);;
return NO_ALARM;
else if(task.hasDueTime()) long dueDateAlarm;
if(task.hasDueTime())
// return due date straight up // return due date straight up
return dueDate; dueDateAlarm = dueDate;
else { else {
// return notification time on this day // return notification time on this day
Date date = new Date(dueDate); Date date = new Date(dueDate);
date.setHours(Preferences.getIntegerFromString(R.string.p_rmd_time, 12)); date.setHours(Preferences.getIntegerFromString(R.string.p_rmd_time, 12));
date.setMinutes(0); date.setMinutes(0);
return date.getTime(); dueDateAlarm = date.getTime();
} }
if(lastReminder > dueDateAlarm)
return NO_ALARM;
if(dueDate > DateUtilities.now() && dueDateAlarm < DateUtilities.now())
dueDateAlarm = dueDate;
return dueDateAlarm;
} }
return NO_ALARM; return NO_ALARM;
} }
/** /**
* Calculate the next alarm time for random reminders. We take the last * Calculate the next alarm time for random reminders.
* random reminder time and add approximately the reminder period, until * <p>
* we get a time that's in the future. * We take the last reminder time and add approximately the reminder
* period. If it's still in the past, we set it to some time in the near
* future.
* *
* @param task * @param task
* @return * @return
@ -242,14 +272,16 @@ public final class ReminderService {
if((reminderPeriod) > 0) { if((reminderPeriod) > 0) {
long when = task.getValue(Task.REMINDER_LAST); long when = task.getValue(Task.REMINDER_LAST);
// get or make up a last notification time if(when == 0)
if(when == 0) { when = task.getValue(Task.CREATION_DATE);
when = DateUtilities.now();
task.setValue(Task.REMINDER_LAST, when);
taskDao.saveExisting(task);
}
when += (long)(reminderPeriod * (0.85f + 0.3f * random.nextFloat())); when += (long)(reminderPeriod * (0.85f + 0.3f * random.nextFloat()));
if(when < DateUtilities.now()) {
when = DateUtilities.now() + (long)((0.5f +
6 * random.nextFloat()) * DateUtilities.ONE_HOUR);
}
return when; return when;
} }
return NO_ALARM; return NO_ALARM;
@ -309,10 +341,8 @@ public final class ReminderService {
if(time == 0 || time == NO_ALARM) if(time == 0 || time == NO_ALARM)
am.cancel(pendingIntent); am.cancel(pendingIntent);
else { else {
if(time < DateUtilities.now()) { if(time < DateUtilities.now())
time = DateUtilities.now() + (long)((0.5f + time = DateUtilities.now() + 5000L;
4 * random.nextFloat()) * DateUtilities.ONE_HOUR);
}
Log.e("Astrid", "Alarm (" + task.getId() + ", " + type + Log.e("Astrid", "Alarm (" + task.getId() + ", " + type +
") set for " + new Date(time)); ") set for " + new Date(time));

@ -17,7 +17,6 @@ import com.google.ical.values.RRule;
import com.google.ical.values.WeekdayNum; import com.google.ical.values.WeekdayNum;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
@ -27,7 +26,7 @@ import com.todoroo.astrid.model.Task;
* @author Tim Su <tim@todoroo.com> * @author Tim Su <tim@todoroo.com>
* *
*/ */
public class RepeatDetailExposer extends BroadcastReceiver implements DetailExposer { public class RepeatDetailExposer extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -119,7 +118,6 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo
return null; return null;
} }
@Override
public String getPluginIdentifier() { public String getPluginIdentifier() {
return RepeatsPlugin.IDENTIFIER; return RepeatsPlugin.IDENTIFIER;
} }

@ -0,0 +1,39 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.rmilk;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.SyncAction;
/**
* Exposes sync action
*
*/
public class MilkSyncActionExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose sync action
if(!MilkUtilities.isLoggedIn())
return;
Intent syncIntent = new Intent(MilkBackgroundService.SYNC_ACTION, null,
context, MilkBackgroundService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, syncIntent, PendingIntent.FLAG_UPDATE_CURRENT);
SyncAction syncAction = new SyncAction(context.getString(R.string.rmilk_MPr_header),
pendingIntent);
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, MilkUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, syncAction);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -8,7 +8,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
/** /**
* Exposes Task Detail for tags, i.e. "Tags: frogs, animals" * Exposes Task Detail for tags, i.e. "Tags: frogs, animals"
@ -16,7 +15,7 @@ import com.todoroo.astrid.api.DetailExposer;
* @author Tim Su <tim@todoroo.com> * @author Tim Su <tim@todoroo.com>
* *
*/ */
public class TagDetailExposer extends BroadcastReceiver implements DetailExposer { public class TagDetailExposer extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -39,7 +38,6 @@ public class TagDetailExposer extends BroadcastReceiver implements DetailExposer
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
} }
@Override
public String getTaskDetails(Context context, long id, boolean extended) { public String getTaskDetails(Context context, long id, boolean extended) {
if(extended) if(extended)
return null; return null;
@ -51,9 +49,4 @@ public class TagDetailExposer extends BroadcastReceiver implements DetailExposer
return "<img src='silk_tag_pink'/> " + tagList; //$NON-NLS-1$ return "<img src='silk_tag_pink'/> " + tagList; //$NON-NLS-1$
} }
@Override
public String getPluginIdentifier() {
return TagsPlugin.IDENTIFIER;
}
} }

@ -11,6 +11,7 @@ import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.view.View;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import com.timsu.astrid.R; import com.timsu.astrid.R;
@ -33,8 +34,12 @@ public class TimerDecorationExposer extends BroadcastReceiver {
private static HashMap<Long, TaskDecoration> decorations = private static HashMap<Long, TaskDecoration> decorations =
new HashMap<Long, TaskDecoration>(); new HashMap<Long, TaskDecoration>();
public static void removeFromCache(long taskId) { private static HashMap<Long, Boolean> tasksNeedingUpdate =
new HashMap<Long, Boolean>();
public static void updateTimer(long taskId) {
decorations.remove(taskId); decorations.remove(taskId);
tasksNeedingUpdate.put(taskId, true);
} }
@Override @Override
@ -45,11 +50,15 @@ public class TimerDecorationExposer extends BroadcastReceiver {
Task task; Task task;
try { try {
task = PluginServices.getTaskService().fetchById(taskId, Task.ELAPSED_SECONDS, Task.TIMER_START); if(tasksNeedingUpdate.containsKey(taskId))
task = PluginServices.getTaskService().fetchById(taskId, Task.ID, Task.ELAPSED_SECONDS, Task.TIMER_START);
else
task = intent.getParcelableExtra("model"); //$NON-NLS-1$
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
return; return;
} }
if(task == null || (task.getValue(Task.ELAPSED_SECONDS) == 0 &&
if(task == null || task.getId() != taskId || (task.getValue(Task.ELAPSED_SECONDS) == 0 &&
task.getValue(Task.TIMER_START) == 0)) task.getValue(Task.TIMER_START) == 0))
return; return;
@ -70,12 +79,16 @@ public class TimerDecorationExposer extends BroadcastReceiver {
elapsed += DateUtilities.now() - task.getValue(Task.TIMER_START); elapsed += DateUtilities.now() - task.getValue(Task.TIMER_START);
decoration.decoration.setChronometer(R.id.timer, SystemClock.elapsedRealtime() - decoration.decoration.setChronometer(R.id.timer, SystemClock.elapsedRealtime() -
elapsed, null, true); elapsed, null, true);
decoration.decoration.setViewVisibility(R.id.timer, View.VISIBLE);
decoration.decoration.setViewVisibility(R.id.label, View.GONE);
} else { } else {
// if timer is not started, make the chronometer just a text label, // if timer is not started, make the chronometer just a text label,
// since we don't want the time to be displayed relative to elapsed // since we don't want the time to be displayed relative to elapsed
String format = buildFormat(elapsed); String format = buildFormat(elapsed);
decoration.decoration.setChronometer(R.id.timer, SystemClock.elapsedRealtime() - decoration.color = 0;
elapsed, format, false); decoration.decoration.setTextViewText(R.id.label, format);
decoration.decoration.setViewVisibility(R.id.timer, View.GONE);
decoration.decoration.setViewVisibility(R.id.label, View.VISIBLE);
} }

@ -53,7 +53,7 @@ public class TimerPlugin extends BroadcastReceiver {
} }
} }
PluginServices.getTaskService().save(task); PluginServices.getTaskService().save(task);
TimerDecorationExposer.removeFromCache(task.getId()); TimerDecorationExposer.updateTimer(task.getId());
// transmit new intents // transmit new intents
Intent intent = new Intent(AstridApiConstants.BROADCAST_REQUEST_ACTIONS); Intent intent = new Intent(AstridApiConstants.BROADCAST_REQUEST_ACTIONS);

@ -15,5 +15,5 @@
--> -->
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0" android:duration="400"/> <translate android:fromXDelta="100%p" android:toXDelta="0" android:duration="200"/>
</set> </set>

@ -15,5 +15,5 @@
--> -->
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p" android:duration="400"/> <translate android:fromXDelta="0" android:toXDelta="-100%p" android:duration="200"/>
</set> </set>

@ -15,5 +15,5 @@
--> -->
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%p" android:toXDelta="0" android:duration="400"/> <translate android:fromXDelta="-100%p" android:toXDelta="0" android:duration="200"/>
</set> </set>

@ -15,5 +15,5 @@
--> -->
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="100%p" android:duration="400"/> <translate android:fromXDelta="0" android:toXDelta="100%p" android:duration="200"/>
</set> </set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

@ -52,12 +52,6 @@
android:paddingTop="20dip" android:paddingTop="20dip"
android:visibility="gone"> android:visibility="gone">
<EditText android:id="@+id/confirmPassword"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_confirmPassword"
android:inputType="textPassword"/>
<EditText android:id="@+id/firstName" <EditText android:id="@+id/firstName"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"

@ -8,13 +8,13 @@
android:paddingBottom="4dip" android:paddingBottom="4dip"
android:paddingLeft="4dip" android:paddingLeft="4dip"
android:paddingRight="4dip" android:paddingRight="4dip"
android:minHeight="40dip"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout android:id="@+id/task_row" <LinearLayout android:id="@+id/task_row"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="100" android:layout_weight="100"
android:minHeight="40dip"
android:orientation="horizontal"> android:orientation="horizontal">
<!-- importance --> <!-- importance -->
@ -62,6 +62,7 @@
<TextView android:id="@+id/extendedDetails" <TextView android:id="@+id/extendedDetails"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="4dip"
android:visibility="gone" /> android:visibility="gone" />
<LinearLayout android:id="@+id/actions" <LinearLayout android:id="@+id/actions"
@ -70,6 +71,19 @@
android:background="#4499bbcc" android:background="#4499bbcc"
android:visibility="gone" android:visibility="gone"
android:paddingTop="4dip" android:paddingTop="4dip"
android:orientation="horizontal" /> android:paddingLeft="4dip"
android:orientation="horizontal">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout> </LinearLayout>

@ -81,7 +81,7 @@
android:background="@android:drawable/divider_horizontal_dark" /> android:background="@android:drawable/divider_horizontal_dark" />
<!-- notes --> <!-- notes -->
<TextView <TextView android:id="@+id/notes_label"
android:paddingTop="5dip" android:paddingTop="5dip"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

@ -15,6 +15,14 @@
android:src="@drawable/timers_decoration" /> android:src="@drawable/timers_decoration" />
<Chronometer android:id="@+id/timer" <Chronometer android:id="@+id/timer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="top"
android:visibility="gone"
android:textSize="10sp" />
<TextView android:id="@+id/label"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2" android:layout_weight="2"

@ -8,6 +8,8 @@
<color name="task_list_importance_3">#ffb8b8b8</color> <color name="task_list_importance_3">#ffb8b8b8</color>
<color name="task_list_importance_4">#ffa0a0a0</color> <color name="task_list_importance_4">#ffa0a0a0</color>
<color name="task_list_selected">#33ffffff</color>
<color name="taskList_dueDateOverdue">#FFFB6666</color> <color name="taskList_dueDateOverdue">#FFFB6666</color>
<color name="taskList_dueDate">#ffF0E89E</color> <color name="taskList_dueDate">#ffF0E89E</color>
<color name="taskList_remainingTime">#ff88AAFF</color> <color name="taskList_remainingTime">#ff88AAFF</color>

@ -57,6 +57,8 @@
<string name="export_toast">Backed Up %s to %s.</string> <string name="export_toast">Backed Up %s to %s.</string>
<string name="export_toast_no_tasks">No Tasks to Export.</string>
<!-- Progress Dialog Title for exporting --> <!-- Progress Dialog Title for exporting -->
<string name="export_progress_title">Exporting...</string> <string name="export_progress_title">Exporting...</string>

@ -162,6 +162,9 @@
<!-- Menu: Adjust Sort and Hidden Task Settings --> <!-- Menu: Adjust Sort and Hidden Task Settings -->
<string name="TLA_menu_sort">Sort &amp; Hidden</string> <string name="TLA_menu_sort">Sort &amp; Hidden</string>
<!-- Menu: Sync Now -->
<string name="TLA_menu_sync">Sync Now!</string>
<!-- Menu: Settings --> <!-- Menu: Settings -->
<string name="TLA_menu_settings">Settings</string> <string name="TLA_menu_settings">Settings</string>
@ -215,6 +218,9 @@ button: add task & go to the edit page.
<!-- Context Item: undelete task --> <!-- Context Item: undelete task -->
<string name="TAd_contextUndeleteTask">Undelete Task</string> <string name="TAd_contextUndeleteTask">Undelete Task</string>
<!-- Context Item: purge task -->
<string name="TAd_contextPurgeTask">Purge Task</string>
<!-- ============================================== SortSelectionDialog == --> <!-- ============================================== SortSelectionDialog == -->
<!-- Sort Selection: dialog title --> <!-- Sort Selection: dialog title -->
@ -519,6 +525,9 @@ to the plugin creator for fastest service.
<!-- Sync Notification: toast when sync activated from activity --> <!-- Sync Notification: toast when sync activated from activity -->
<string name="SyP_progress_toast">Synchronizing...</string> <string name="SyP_progress_toast">Synchronizing...</string>
<!-- Sync Label: used in menu to denote synchronization -->
<string name="SyP_label">Synchronization</string>
<!-- ====================================================== TasksWidget == --> <!-- ====================================================== TasksWidget == -->
<!-- Widget text when loading tasks --> <!-- Widget text when loading tasks -->

@ -16,6 +16,12 @@
<!-- Producteev dashboard filter title (%s => dashboardname) --> <!-- Producteev dashboard filter title (%s => dashboardname) -->
<string name="producteev_FEx_responsible_title">Assigned To \'%s\'</string> <string name="producteev_FEx_responsible_title">Assigned To \'%s\'</string>
<!-- detail for showing tasks created by someone else (%s => person name) -->
<string name="producteev_PDE_task_from">from %s</string>
<!-- replacement string for task edit "Notes" when using Producteev -->
<string name="producteev_TEA_notes">Add a Comment</string>
<!-- ==================================================== Preferences == --> <!-- ==================================================== Preferences == -->
<!-- Preferences Title: Producteev --> <!-- Preferences Title: Producteev -->
@ -27,6 +33,12 @@
<!-- dashboard title for tasks that are not synchronized --> <!-- dashboard title for tasks that are not synchronized -->
<string name="producteev_no_dashboard">Do Not Synchronize</string> <string name="producteev_no_dashboard">Do Not Synchronize</string>
<!-- dashboard spinner entry on TEA for adding a new dashboard -->
<string name="producteev_create_dashboard">Add new Workspace...</string>
<!-- dashboard spinner entry on TEA for adding a new dashboard -->
<string name="producteev_create_dashboard_name">Name for new Workspace</string>
<!-- preference title for default dashboard --> <!-- preference title for default dashboard -->
<string name="producteev_PPr_defaultdash_title">Default Workspace</string> <string name="producteev_PPr_defaultdash_title">Default Workspace</string>
@ -80,8 +92,11 @@
<!-- ================================================ Synchronization == --> <!-- ================================================ Synchronization == -->
<!-- title for notification tray when synchronizing --> <!-- title for notification tray after synchronizing -->
<string name="producteev_notification_title">Astrid: Producteev</string> <string name="producteev_notification_title">Producteev</string>
<!-- text for notification tray when synchronizing -->
<string name="producteev_notification_text">%s tasks updated / click for more details</string>
<!-- Error msg when io exception --> <!-- Error msg when io exception -->
<string name="producteev_ioerror">Connection Error! Check your Internet connection.</string> <string name="producteev_ioerror">Connection Error! Check your Internet connection.</string>

@ -14,9 +14,10 @@ import android.content.pm.ResolveInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceCategory; import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.preference.Preference.OnPreferenceClickListener;
import android.widget.Toast;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.Autowired;
@ -29,6 +30,8 @@ import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.producteev.ProducteevPreferences;
import com.todoroo.astrid.rmilk.MilkPreferences;
import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
@ -92,6 +95,12 @@ public class EditPreferences extends TodorooPreferences {
preference.setIntent(intent); preference.setIntent(intent);
String application = resolveInfo.activityInfo.applicationInfo.loadLabel(pm).toString(); String application = resolveInfo.activityInfo.applicationInfo.loadLabel(pm).toString();
// temporary overrides
if(ProducteevPreferences.class.getName().equals(resolveInfo.activityInfo.name) ||
MilkPreferences.class.getName().equals(resolveInfo.activityInfo.name))
application = getString(R.string.SyP_label);
if(!applicationPreferences.containsKey(application)) if(!applicationPreferences.containsKey(application))
applicationPreferences.put(application, new ArrayList<Preference>()); applicationPreferences.put(application, new ArrayList<Preference>());
ArrayList<Preference> arrayList = applicationPreferences.get(application); ArrayList<Preference> arrayList = applicationPreferences.get(application);
@ -122,6 +131,18 @@ public class EditPreferences extends TodorooPreferences {
getPreferenceScreen().addPreference(group); getPreferenceScreen().addPreference(group);
Preference preference = new Preference(this); Preference preference = new Preference(this);
preference.setTitle("Flush detail cache");
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference p) {
database.openForWriting();
Toast.makeText(EditPreferences.this, "" + taskService.clearDetails(),
Toast.LENGTH_LONG).show();
return false;
}
});
group.addPreference(preference);
preference = new Preference(this);
preference.setTitle("Make Lots of Tasks"); preference.setTitle("Make Lots of Tasks");
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference p) { public boolean onPreferenceClick(Preference p) {

@ -99,13 +99,14 @@ public class SortSelectionActivity {
break; break;
case SortSelectionActivity.SORT_DUE: case SortSelectionActivity.SORT_DUE:
order = Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0), order = Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0),
DateUtilities.now()*2, Task.DUE_DATE) + "+" + Task.IMPORTANCE); DateUtilities.now()*2, Task.DUE_DATE) + "+" + Task.IMPORTANCE +
"+3*" + Task.COMPLETION_DATE);
break; break;
case SortSelectionActivity.SORT_IMPORTANCE: case SortSelectionActivity.SORT_IMPORTANCE:
order = Order.asc(Task.IMPORTANCE + "*" + (2*DateUtilities.now()) + //$NON-NLS-1$ order = Order.asc(Task.IMPORTANCE + "*" + (2*DateUtilities.now()) + //$NON-NLS-1$
"+" + Functions.caseStatement(Task.DUE_DATE.eq(0), //$NON-NLS-1$ "+" + Functions.caseStatement(Task.DUE_DATE.eq(0), //$NON-NLS-1$
Functions.now() + "+" + DateUtilities.ONE_WEEK, //$NON-NLS-1$ Functions.now() + "+" + DateUtilities.ONE_WEEK, //$NON-NLS-1$
Task.DUE_DATE)); Task.DUE_DATE) + "+8*" + Task.COMPLETION_DATE);
break; break;
case SortSelectionActivity.SORT_MODIFIED: case SortSelectionActivity.SORT_MODIFIED:
order = Order.desc(Task.MODIFICATION_DATE); order = Order.desc(Task.MODIFICATION_DATE);

@ -58,6 +58,7 @@ import android.widget.LinearLayout;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TabHost; import android.widget.TabHost;
import android.widget.TextView;
import android.widget.TimePicker; import android.widget.TimePicker;
import android.widget.Toast; import android.widget.Toast;
import android.widget.ToggleButton; import android.widget.ToggleButton;
@ -74,10 +75,10 @@ import com.todoroo.astrid.alarms.AlarmControlSet;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.gcal.GCalControlSet; import com.todoroo.astrid.gcal.GCalControlSet;
import com.todoroo.astrid.model.AddOn;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.producteev.ProducteevControlSet; import com.todoroo.astrid.producteev.ProducteevControlSet;
import com.todoroo.astrid.producteev.ProducteevUtilities; import com.todoroo.astrid.producteev.ProducteevUtilities;
import com.todoroo.astrid.reminders.Notifications;
import com.todoroo.astrid.repeats.RepeatControlSet; import com.todoroo.astrid.repeats.RepeatControlSet;
import com.todoroo.astrid.service.AddOnService; import com.todoroo.astrid.service.AddOnService;
import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.MetadataService;
@ -250,9 +251,10 @@ public final class TaskEditActivity extends TabActivity {
LinearLayout addonsAddons = (LinearLayout) findViewById(R.id.tab_addons_addons); LinearLayout addonsAddons = (LinearLayout) findViewById(R.id.tab_addons_addons);
try { try {
AddOn producteevAddon = addOnService.getAddOn(AddOnService.PRODUCTEEV_PACKAGE, "Producteev"); //$NON-NLS-1$ if(ProducteevUtilities.INSTANCE.isLoggedIn()) {
if (addOnService.isInstalled(producteevAddon) && ProducteevUtilities.INSTANCE.isLoggedIn()) {
controls.add(new ProducteevControlSet(this, addonsAddons)); controls.add(new ProducteevControlSet(this, addonsAddons));
((TextView)findViewById(R.id.notes)).setHint(R.string.producteev_TEA_notes);
((TextView)findViewById(R.id.notes_label)).setHint(R.string.producteev_TEA_notes);
} }
} catch (Exception e) { } catch (Exception e) {
Log.e("astrid-error", "loading-control-set", e); //$NON-NLS-1$ //$NON-NLS-2$ Log.e("astrid-error", "loading-control-set", e); //$NON-NLS-1$ //$NON-NLS-2$
@ -357,6 +359,9 @@ public final class TaskEditActivity extends TabActivity {
finish(); finish();
return; return;
} }
// clear notification
Notifications.cancelNotifications(model.getId());
} }
/** Populate UI component values from the model */ /** Populate UI component values from the model */
@ -790,7 +795,7 @@ public final class TaskEditActivity extends TabActivity {
Task.URGENCY_TOMORROW); Task.URGENCY_TOMORROW);
String dayAfterTomorrow = DateUtils.getDayOfWeekString( String dayAfterTomorrow = DateUtils.getDayOfWeekString(
new Date(DateUtilities.now() + 2 * DateUtilities.ONE_DAY).getDay() + new Date(DateUtilities.now() + 2 * DateUtilities.ONE_DAY).getDay() +
Calendar.SUNDAY, 0); Calendar.SUNDAY, DateUtils.FORMAT_ABBREV_ALL);
urgencyValues[3] = new UrgencyValue(dayAfterTomorrow, urgencyValues[3] = new UrgencyValue(dayAfterTomorrow,
Task.URGENCY_DAY_AFTER); Task.URGENCY_DAY_AFTER);
urgencyValues[4] = new UrgencyValue(labels[4], urgencyValues[4] = new UrgencyValue(labels[4],

@ -1,6 +1,7 @@
package com.todoroo.astrid.activity; package com.todoroo.astrid.activity;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Timer; import java.util.Timer;
@ -9,6 +10,7 @@ import java.util.concurrent.atomic.AtomicReference;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.ListActivity; import android.app.ListActivity;
import android.app.PendingIntent.CanceledException;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
@ -38,6 +40,7 @@ import android.view.inputmethod.EditorInfo;
import android.widget.AbsListView; import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener; import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ArrayAdapter;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
@ -65,6 +68,7 @@ import com.todoroo.astrid.adapter.TaskAdapter.ViewHolder;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.PermaSql; import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.api.SyncAction;
import com.todoroo.astrid.api.TaskAction; import com.todoroo.astrid.api.TaskAction;
import com.todoroo.astrid.api.TaskDecoration; import com.todoroo.astrid.api.TaskDecoration;
import com.todoroo.astrid.backup.BackupActivity; import com.todoroo.astrid.backup.BackupActivity;
@ -77,6 +81,7 @@ import com.todoroo.astrid.reminders.Notifications;
import com.todoroo.astrid.reminders.ReminderService; import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.reminders.ReminderService.AlarmScheduler; import com.todoroo.astrid.reminders.ReminderService.AlarmScheduler;
import com.todoroo.astrid.service.AddOnService; import com.todoroo.astrid.service.AddOnService;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.TaskService;
@ -108,16 +113,17 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
private static final int MENU_ADDONS_ID = Menu.FIRST + 1; private static final int MENU_ADDONS_ID = Menu.FIRST + 1;
private static final int MENU_SETTINGS_ID = Menu.FIRST + 2; private static final int MENU_SETTINGS_ID = Menu.FIRST + 2;
private static final int MENU_SORT_ID = Menu.FIRST + 3; private static final int MENU_SORT_ID = Menu.FIRST + 3;
private static final int MENU_HELP_ID = Menu.FIRST + 4; private static final int MENU_SYNC_ID = Menu.FIRST + 4;
private static final int MENU_ADDON_INTENT_ID = Menu.FIRST + 5; private static final int MENU_HELP_ID = Menu.FIRST + 5;
private static final int MENU_ADDON_INTENT_ID = Menu.FIRST + 6;
private static final int CONTEXT_MENU_EDIT_TASK_ID = Menu.FIRST + 6; private static final int CONTEXT_MENU_EDIT_TASK_ID = Menu.FIRST + 20;
private static final int CONTEXT_MENU_DELETE_TASK_ID = Menu.FIRST + 7; private static final int CONTEXT_MENU_DELETE_TASK_ID = Menu.FIRST + 21;
private static final int CONTEXT_MENU_UNDELETE_TASK_ID = Menu.FIRST + 8; private static final int CONTEXT_MENU_UNDELETE_TASK_ID = Menu.FIRST + 22;
private static final int CONTEXT_MENU_ADDON_INTENT_ID = Menu.FIRST + 9; private static final int CONTEXT_MENU_PURGE_TASK_ID = Menu.FIRST + 23;
private static final int CONTEXT_MENU_ADDON_INTENT_ID = Menu.FIRST + 24;
/** menu code indicating the end of the context menu */ private static final int CONTEXT_MENU_DEBUG = Menu.FIRST + 30;
private static final int CONTEXT_MENU_DEBUG = Menu.FIRST + 10;
// --- constants // --- constants
@ -147,6 +153,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
protected TaskAdapter taskAdapter = null; protected TaskAdapter taskAdapter = null;
protected DetailReceiver detailReceiver = new DetailReceiver(); protected DetailReceiver detailReceiver = new DetailReceiver();
protected RefreshReceiver refreshReceiver = new RefreshReceiver(); protected RefreshReceiver refreshReceiver = new RefreshReceiver();
protected SyncActionReceiver syncActionReceiver = new SyncActionReceiver();
private ImageButton quickAddButton; private ImageButton quickAddButton;
private EditText quickAddBox; private EditText quickAddBox;
@ -155,11 +162,16 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
private int sortSort; private int sortSort;
private final AtomicReference<String> sqlQueryTemplate = new AtomicReference<String>(); private final AtomicReference<String> sqlQueryTemplate = new AtomicReference<String>();
private Timer backgroundTimer; private Timer backgroundTimer;
private final LinkedHashSet<SyncAction> syncActions = new LinkedHashSet<SyncAction>();
/* ====================================================================== /* ======================================================================
* ======================================================= initialization * ======================================================= initialization
* ====================================================================== */ * ====================================================================== */
static {
AstridDependencyInjector.initialize();
}
public TaskListActivity() { public TaskListActivity() {
DependencyInjectionService.getInstance().inject(this); DependencyInjectionService.getInstance().inject(this);
} }
@ -176,13 +188,6 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
else else
setContentView(R.layout.task_list_activity_api3); setContentView(R.layout.task_list_activity_api3);
Bundle extras = getIntent().getExtras();
if(extras != null && extras.containsKey(TOKEN_FILTER)) {
filter = extras.getParcelable(TOKEN_FILTER);
} else {
filter = CoreFilterExposer.buildInboxFilter(getResources());
}
if(database == null) if(database == null)
return; return;
@ -196,6 +201,14 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
Bundle extras = intent.getExtras();
if(extras != null && extras.containsKey(TOKEN_FILTER)) {
filter = extras.getParcelable(TOKEN_FILTER);
} else {
filter = CoreFilterExposer.buildInboxFilter(getResources());
}
setUpTaskList(); setUpTaskList();
if(Constants.DEBUG) if(Constants.DEBUG)
setTitle("[D] " + filter.title); //$NON-NLS-1$ setTitle("[D] " + filter.title); //$NON-NLS-1$
@ -232,6 +245,12 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
R.string.TLA_menu_sort); R.string.TLA_menu_sort);
item.setIcon(android.R.drawable.ic_menu_sort_by_size); item.setIcon(android.R.drawable.ic_menu_sort_by_size);
if(syncActions.size() > 0) {
item = menu.add(Menu.NONE, MENU_SYNC_ID, Menu.NONE,
R.string.TLA_menu_sync);
item.setIcon(R.drawable.ic_menu_refresh);
}
item = menu.add(Menu.NONE, MENU_HELP_ID, Menu.NONE, item = menu.add(Menu.NONE, MENU_HELP_ID, Menu.NONE,
R.string.TLA_menu_help); R.string.TLA_menu_help);
item.setIcon(android.R.drawable.ic_menu_help); item.setIcon(android.R.drawable.ic_menu_help);
@ -266,11 +285,12 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
Intent intent = new Intent(TaskListActivity.this, Intent intent = new Intent(TaskListActivity.this,
FilterListActivity.class); FilterListActivity.class);
startActivity(intent); startActivity(intent);
if(AndroidUtilities.getSdkVersion() >= 5) {
//overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out);
}
} }
}); });
((TextView)findViewById(R.id.listLabel)).setText(filter.title);
// set listener for quick-changing task priority // set listener for quick-changing task priority
getListView().setOnKeyListener(new OnKeyListener() { getListView().setOnKeyListener(new OnKeyListener() {
@Override @Override
@ -357,7 +377,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
} }
}); });
// gestures // gestures / animation
try { try {
GestureService.registerGestureDetector(this, R.id.gestures, R.raw.gestures, this); GestureService.registerGestureDetector(this, R.id.gestures, R.raw.gestures, this);
} catch (VerifyError e) { } catch (VerifyError e) {
@ -418,6 +438,8 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
new IntentFilter(AstridApiConstants.BROADCAST_SEND_ACTIONS)); new IntentFilter(AstridApiConstants.BROADCAST_SEND_ACTIONS));
registerReceiver(refreshReceiver, registerReceiver(refreshReceiver,
new IntentFilter(AstridApiConstants.BROADCAST_EVENT_REFRESH)); new IntentFilter(AstridApiConstants.BROADCAST_EVENT_REFRESH));
registerReceiver(syncActionReceiver,
new IntentFilter(AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS));
setUpBackgroundJobs(); setUpBackgroundJobs();
} }
@ -438,6 +460,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
protected class RefreshReceiver extends BroadcastReceiver { protected class RefreshReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if(intent == null || !AstridApiConstants.BROADCAST_EVENT_REFRESH.equals(intent.getAction()))
return;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -448,6 +473,29 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
} }
} }
/**
* Receiver which receives sync provider intents
*
* @author Tim Su <tim@todoroo.com>
*
*/
protected class SyncActionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if(intent == null || !AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS.equals(intent.getAction()))
return;
try {
Bundle extras = intent.getExtras();
SyncAction syncAction = extras.getParcelable(AstridApiConstants.EXTRAS_RESPONSE);
syncActions.add(syncAction);
} catch (Exception e) {
exceptionService.reportError("receive-sync-action-" + //$NON-NLS-1$
intent.getStringExtra(AstridApiConstants.EXTRAS_ADDON), e);
}
}
}
/** /**
* Receiver which receives detail or decoration intents * Receiver which receives detail or decoration intents
* *
@ -468,9 +516,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
} else if(AstridApiConstants.BROADCAST_SEND_DETAILS.equals(intent.getAction())) { } else if(AstridApiConstants.BROADCAST_SEND_DETAILS.equals(intent.getAction())) {
String detail = extras.getString(AstridApiConstants.EXTRAS_RESPONSE); String detail = extras.getString(AstridApiConstants.EXTRAS_RESPONSE);
if(extras.getBoolean(AstridApiConstants.EXTRAS_EXTENDED)) if(extras.getBoolean(AstridApiConstants.EXTRAS_EXTENDED))
taskAdapter.detailManager.addNew(taskId, addOn, detail);
else
taskAdapter.extendedDetailManager.addNew(taskId, addOn, detail); taskAdapter.extendedDetailManager.addNew(taskId, addOn, detail);
else
taskAdapter.addDetails(taskId, detail);
} else if(AstridApiConstants.BROADCAST_SEND_ACTIONS.equals(intent.getAction())) { } else if(AstridApiConstants.BROADCAST_SEND_ACTIONS.equals(intent.getAction())) {
TaskAction action = extras.getParcelable(AstridApiConstants.EXTRAS_RESPONSE); TaskAction action = extras.getParcelable(AstridApiConstants.EXTRAS_RESPONSE);
taskAdapter.taskActionManager.addNew(taskId, addOn, action); taskAdapter.taskActionManager.addNew(taskId, addOn, action);
@ -551,6 +599,11 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
if(oldListItemSelected != ListView.INVALID_POSITION && if(oldListItemSelected != ListView.INVALID_POSITION &&
oldListItemSelected < taskCursor.getCount()) oldListItemSelected < taskCursor.getCount())
getListView().setSelection(oldListItemSelected); getListView().setSelection(oldListItemSelected);
// also load sync actions
syncActions.clear();
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_SYNC_ACTIONS);
sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
} }
/** /**
@ -561,6 +614,8 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
sqlQueryTemplate.set(SortSelectionActivity.adjustQueryForFlagsAndSort(filter.sqlQuery, sqlQueryTemplate.set(SortSelectionActivity.adjustQueryForFlagsAndSort(filter.sqlQuery,
sortFlags, sortSort)); sortFlags, sortSort));
((TextView)findViewById(R.id.listLabel)).setText(filter.title);
// perform query // perform query
TodorooCursor<Task> currentCursor = taskService.fetchFiltered( TodorooCursor<Task> currentCursor = taskService.fetchFiltered(
sqlQueryTemplate.get(), null, TaskAdapter.PROPERTIES); sqlQueryTemplate.get(), null, TaskAdapter.PROPERTIES);
@ -723,6 +778,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
if(task.isDeleted()) { if(task.isDeleted()) {
menu.add(id, CONTEXT_MENU_UNDELETE_TASK_ID, Menu.NONE, menu.add(id, CONTEXT_MENU_UNDELETE_TASK_ID, Menu.NONE,
R.string.TAd_contextUndeleteTask); R.string.TAd_contextUndeleteTask);
menu.add(id, CONTEXT_MENU_PURGE_TASK_ID, Menu.NONE,
R.string.TAd_contextPurgeTask);
} else { } else {
menu.add(id, CONTEXT_MENU_EDIT_TASK_ID, Menu.NONE, menu.add(id, CONTEXT_MENU_EDIT_TASK_ID, Menu.NONE,
R.string.TAd_contextEditTask); R.string.TAd_contextEditTask);
@ -768,6 +826,44 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
.show(); .show();
} }
private void performSyncAction() {
if(syncActions.size() == 0)
return;
if(syncActions.size() == 1) {
SyncAction syncAction = syncActions.iterator().next();
try {
syncAction.intent.send();
Toast.makeText(this, R.string.SyP_progress_toast,
Toast.LENGTH_LONG).show();
} catch (CanceledException e) {
//
}
} else {
final SyncAction[] actions = syncActions.toArray(new SyncAction[syncActions.size()]);
ArrayAdapter<SyncAction> adapter = new ArrayAdapter<SyncAction>(this,
android.R.layout.simple_spinner_dropdown_item, actions);
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface click, int which) {
try {
actions[which].intent.send();
Toast.makeText(TaskListActivity.this, R.string.SyP_progress_toast,
Toast.LENGTH_LONG).show();
} catch (CanceledException e) {
//
}
}
};
// show a menu of available options
new AlertDialog.Builder(this)
.setTitle(R.string.SyP_label)
.setAdapter(adapter, listener)
.show().setOwnerActivity(this);
}
}
@Override @Override
public boolean onMenuItemSelected(int featureId, final MenuItem item) { public boolean onMenuItemSelected(int featureId, final MenuItem item) {
Intent intent; Intent intent;
@ -788,6 +884,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
this, sortFlags, sortSort); this, sortFlags, sortSort);
dialog.show(); dialog.show();
return true; return true;
case MENU_SYNC_ID:
performSyncAction();
return true;
case MENU_HELP_ID: case MENU_HELP_ID:
intent = new Intent(Intent.ACTION_VIEW, intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("http://weloveastrid.com/help-user-guide-astrid-v3/active-tasks/")); //$NON-NLS-1$ Uri.parse("http://weloveastrid.com/help-user-guide-astrid-v3/active-tasks/")); //$NON-NLS-1$
@ -832,6 +931,13 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
return true; return true;
} }
case CONTEXT_MENU_PURGE_TASK_ID: {
itemId = item.getGroupId();
taskService.purge(itemId);
loadTaskListContent(true);
return true;
}
// --- debug // --- debug
case CONTEXT_MENU_DEBUG: { case CONTEXT_MENU_DEBUG: {
@ -842,6 +948,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
ReminderService.getInstance().setScheduler(new AlarmScheduler() { ReminderService.getInstance().setScheduler(new AlarmScheduler() {
@Override @Override
public void createAlarm(Task theTask, long time, int type) { public void createAlarm(Task theTask, long time, int type) {
if(time == 0 || time == Long.MAX_VALUE)
return;
Toast.makeText(TaskListActivity.this, "Scheduled Alarm: " + //$NON-NLS-1$ Toast.makeText(TaskListActivity.this, "Scheduled Alarm: " + //$NON-NLS-1$
new Date(time), Toast.LENGTH_LONG).show(); new Date(time), Toast.LENGTH_LONG).show();
ReminderService.getInstance().setScheduler(null); ReminderService.getInstance().setScheduler(null);
@ -872,6 +981,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
Intent intent = new Intent(TaskListActivity.this, Intent intent = new Intent(TaskListActivity.this,
FilterListActivity.class); FilterListActivity.class);
startActivity(intent); startActivity(intent);
if(AndroidUtilities.getSdkVersion() >= 5) {
// overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out);
}
} }
} }

@ -1,6 +1,7 @@
package com.todoroo.astrid.adapter; package com.todoroo.astrid.adapter;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -16,6 +17,7 @@ import android.graphics.Paint;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.Html; import android.text.Html;
import android.text.Html.ImageGetter; import android.text.Html.ImageGetter;
import android.text.TextUtils;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
@ -40,20 +42,17 @@ import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.SoftHashMap;
import com.todoroo.astrid.activity.TaskEditActivity; import com.todoroo.astrid.activity.TaskEditActivity;
import com.todoroo.astrid.activity.TaskListActivity; import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.alarms.AlarmDetailExposer;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.DetailExposer;
import com.todoroo.astrid.api.TaskAction; import com.todoroo.astrid.api.TaskAction;
import com.todoroo.astrid.api.TaskDecoration; import com.todoroo.astrid.api.TaskDecoration;
import com.todoroo.astrid.model.Task; import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.notes.NoteDetailExposer; import com.todoroo.astrid.notes.NoteDetailExposer;
import com.todoroo.astrid.producteev.ProducteevDetailExposer; import com.todoroo.astrid.producteev.ProducteevDetailExposer;
import com.todoroo.astrid.repeats.RepeatDetailExposer; import com.todoroo.astrid.repeats.RepeatDetailExposer;
import com.todoroo.astrid.service.AddOnService;
import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.tags.TagDetailExposer;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Preferences; import com.todoroo.astrid.utility.Preferences;
@ -71,6 +70,8 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
public static final String DETAIL_SEPARATOR = " | "; //$NON-NLS-1$ public static final String DETAIL_SEPARATOR = " | "; //$NON-NLS-1$
public static final String BROADCAST_EXTRA_TASK = "model"; //$NON-NLS-1$
// --- other constants // --- other constants
/** Properties that need to be read from the action item */ /** Properties that need to be read from the action item */
@ -82,15 +83,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
Task.COMPLETION_DATE, Task.COMPLETION_DATE,
Task.HIDE_UNTIL, Task.HIDE_UNTIL,
Task.DELETION_DATE, Task.DELETION_DATE,
}; Task.DETAILS,
Task.ELAPSED_SECONDS,
/** Internal Task Detail exposers */ Task.TIMER_START,
public static final DetailExposer[] EXPOSERS = new DetailExposer[] {
new TagDetailExposer(),
new RepeatDetailExposer(),
new NoteDetailExposer(),
new ProducteevDetailExposer(),
new AlarmDetailExposer(),
}; };
private static int[] IMPORTANCE_COLORS = null; private static int[] IMPORTANCE_COLORS = null;
@ -103,6 +98,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
@Autowired @Autowired
private TaskService taskService; private TaskService taskService;
@Autowired
private AddOnService addOnService;
protected final ListActivity activity; protected final ListActivity activity;
protected final HashMap<Long, Boolean> completedItems = new HashMap<Long, Boolean>(); protected final HashMap<Long, Boolean> completedItems = new HashMap<Long, Boolean>();
private OnCompletedTaskListener onCompletedTaskListener = null; private OnCompletedTaskListener onCompletedTaskListener = null;
@ -110,6 +108,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
private final int resource; private final int resource;
private final LayoutInflater inflater; private final LayoutInflater inflater;
private int fontSize; private int fontSize;
private DetailLoaderThread detailLoader;
private final AtomicReference<String> query; private final AtomicReference<String> query;
@ -118,8 +117,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
// --- task detail and decoration soft caches // --- task detail and decoration soft caches
public final DetailManager detailManager = new DetailManager(false); public final DetailManager extendedDetailManager = new DetailManager();
public final DetailManager extendedDetailManager = new DetailManager(true);
public final DecorationManager decorationManager = public final DecorationManager decorationManager =
new DecorationManager(); new DecorationManager();
public final TaskActionManager taskActionManager = new TaskActionManager(); public final TaskActionManager taskActionManager = new TaskActionManager();
@ -157,6 +155,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
if(IMPORTANCE_COLORS == null) if(IMPORTANCE_COLORS == null)
IMPORTANCE_COLORS = Task.getImportanceColors(activity.getResources()); IMPORTANCE_COLORS = Task.getImportanceColors(activity.getResources());
} }
detailLoader = new DetailLoaderThread();
detailLoader.start();
} }
/* ====================================================================== /* ======================================================================
@ -219,6 +220,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
ViewHolder viewHolder = ((ViewHolder)view.getTag()); ViewHolder viewHolder = ((ViewHolder)view.getTag());
Task task = viewHolder.task; Task task = viewHolder.task;
task.clear();
task.readFromCursor(cursor); task.readFromCursor(cursor);
setFieldContentsAndVisibility(view); setFieldContentsAndVisibility(view);
@ -269,6 +271,8 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
if(hiddenUntil > DateUtilities.now()) if(hiddenUntil > DateUtilities.now())
nameValue = r.getString(R.string.TAd_hiddenFormat, nameValue); nameValue = r.getString(R.string.TAd_hiddenFormat, nameValue);
nameView.setText(nameValue); nameView.setText(nameValue);
nameView.setMovementMethod(null);
Linkify.addLinks(nameView, Linkify.ALL); Linkify.addLinks(nameView, Linkify.ALL);
} }
@ -322,9 +326,21 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
importanceView.setBackgroundColor(0); importanceView.setBackgroundColor(0);
} }
String details;
if(taskDetailLoader.containsKey(task.getId()))
details = taskDetailLoader.get(task.getId()).toString();
else
details = task.getValue(Task.DETAILS);
if(TextUtils.isEmpty(details) || DETAIL_SEPARATOR.equals(details)) {
viewHolder.details.setVisibility(View.GONE);
} else {
viewHolder.details.setVisibility(View.VISIBLE);
viewHolder.details.setText(Html.fromHtml(details.trim().replace("\n", //$NON-NLS-1$
"<br>"), detailImageGetter, null)); //$NON-NLS-1$
}
// details and decorations, expanded // details and decorations, expanded
if(!isFling) { if(!isFling) {
detailManager.request(viewHolder);
decorationManager.request(viewHolder); decorationManager.request(viewHolder);
if(expanded == task.getId()) { if(expanded == task.getId()) {
extendedDetailManager.request(viewHolder); extendedDetailManager.request(viewHolder);
@ -335,8 +351,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
} }
} else { } else {
long taskId = viewHolder.task.getId(); long taskId = viewHolder.task.getId();
detailManager.reset(viewHolder, taskId);
decorationManager.reset(viewHolder, taskId); decorationManager.reset(viewHolder, taskId);
viewHolder.extendedDetails.setVisibility(View.GONE);
viewHolder.actions.setVisibility(View.GONE);
} }
} }
@ -346,9 +363,10 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
* created. * created.
*/ */
private void addListeners(final View container) { private void addListeners(final View container) {
ViewHolder viewHolder = (ViewHolder)container.getTag();
// check box listener // check box listener
final CheckBox completeBox = ((CheckBox)container.findViewById(R.id.completeBox)); viewHolder.completeBox.setOnClickListener(completeBoxListener);
completeBox.setOnClickListener(completeBoxListener);
// context menu listener // context menu listener
container.setOnCreateContextMenuListener(listener); container.setOnCreateContextMenuListener(listener);
@ -358,27 +376,81 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
} }
/* ====================================================================== /* ======================================================================
* ============================================================== add-ons * ============================================================== details
* ====================================================================== */ * ====================================================================== */
/** // implementation note: this map is really costly if users have
* Called to tell the cache to be cleared // a large number of tasks to load, since it all goes into memory.
*/ // it's best to do this, though, in order to append details to each other
public void flushCaches() { private final Map<Long, StringBuilder> taskDetailLoader = Collections.synchronizedMap(new HashMap<Long, StringBuilder>());
detailManager.clearCache();
extendedDetailManager.clearCache(); private final Task taskDetailContainer = new Task();
decorationManager.clearCache();
taskActionManager.clearCache(); public class DetailLoaderThread extends Thread {
@Override
public void run() {
// for all of the tasks returned by our cursor, verify details
TodorooCursor<Task> fetchCursor = taskService.fetchFiltered(
query.get(), null, Task.ID, Task.DETAILS, Task.COMPLETION_DATE);
activity.startManagingCursor(fetchCursor);
try {
Task task = new Task();
for(fetchCursor.moveToFirst(); !fetchCursor.isAfterLast(); fetchCursor.moveToNext()) {
task.clear();
task.readFromCursor(fetchCursor);
if(task.isCompleted())
continue;
if(TextUtils.isEmpty(task.getValue(Task.DETAILS))) {
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, false);
activity.sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
taskDetailLoader.put(task.getId(), new StringBuilder());
task.setValue(Task.DETAILS, DETAIL_SEPARATOR);
taskService.save(task);
}
}
} catch (Exception e) {
// suppress silently
}
}
} }
/** /**
* AddOnManager for Details * Add detail to a task
* @author Tim Su <tim@todoroo.com>
* *
* @param id
* @param detail
*/ */
public class DetailManager extends AddOnManager<String> { public void addDetails(long id, String detail) {
final StringBuilder details = taskDetailLoader.get(id);
if(details == null)
return;
synchronized(details) {
if(details.toString().contains(detail))
return;
if(details.length() > 0)
details.append(DETAIL_SEPARATOR);
details.append(detail);
taskDetailContainer.setId(id);
taskDetailContainer.setValue(Task.DETAILS, details.toString());
taskService.save(taskDetailContainer);
}
private final ImageGetter imageGetter = new ImageGetter() { activity.runOnUiThread(new Runnable() {
@Override
public void run() {
ListView listView = activity.getListView();
int scrollPos = listView.getScrollY();
notifyDataSetInvalidated();
listView.scrollTo(0, scrollPos);
}
});
}
private final ImageGetter detailImageGetter = new ImageGetter() {
public Drawable getDrawable(String source) { public Drawable getDrawable(String source) {
Resources r = activity.getResources(); Resources r = activity.getResources();
int drawable = r.getIdentifier("drawable/" + source, null, Constants.PACKAGE); //$NON-NLS-1$ int drawable = r.getIdentifier("drawable/" + source, null, Constants.PACKAGE); //$NON-NLS-1$
@ -390,50 +462,54 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
} }
}; };
private final boolean extended; /* ======================================================================
public DetailManager(boolean extended) { * ============================================================== add-ons
this.extended = extended; * ====================================================================== */
/**
* Called to tell the cache to be cleared
*/
public void flushCaches() {
extendedDetailManager.clearCache();
decorationManager.clearCache();
taskActionManager.clearCache();
taskDetailLoader.clear();
detailLoader = new DetailLoaderThread();
detailLoader.start();
}
/**
* Called to tell the cache to be cleared
*/
public void flushSpecific(long taskId) {
extendedDetailManager.clearCache(taskId);
decorationManager.clearCache(taskId);
taskActionManager.clearCache(taskId);
taskDetailLoader.remove(taskId);
}
/**
* AddOnManager for Details
* @author Tim Su <tim@todoroo.com>
*
*/
public class DetailManager extends AddOnManager<String> {
public DetailManager() {
//
} }
@Override @Override
Intent createBroadcastIntent(long taskId) { Intent createBroadcastIntent(Task task) {
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DETAILS); Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, true);
return broadcastIntent; return broadcastIntent;
} }
@Override @Override
public boolean request(final ViewHolder viewHolder) { public void addNew(long taskId, String addOn, String item) {
if(super.request(viewHolder)) { super.addNew(taskId, addOn, item);
final long taskId = viewHolder.task.getId();
// load internal details
new Thread() {
@Override
public void run() {
for(DetailExposer exposer : EXPOSERS) {
final String detail = exposer.getTaskDetails(activity,
taskId, extended);
if(detail == null)
continue;
final Collection<String> cacheList =
addIfNotExists(taskId, exposer.getPluginIdentifier(),
detail);
if(cacheList != null) {
if(taskId != viewHolder.task.getId())
continue;
activity.runOnUiThread(new Runnable() {
public void run() {
draw(viewHolder, taskId, cacheList);
}
});
}
}
};
}.start();
return true;
}
return false;
} }
@SuppressWarnings("nls") @SuppressWarnings("nls")
@ -441,9 +517,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
void draw(ViewHolder viewHolder, long taskId, Collection<String> details) { void draw(ViewHolder viewHolder, long taskId, Collection<String> details) {
if(details == null || viewHolder.task.getId() != taskId) if(details == null || viewHolder.task.getId() != taskId)
return; return;
TextView view = extended ? viewHolder.extendedDetails : viewHolder.details; TextView view = viewHolder.extendedDetails;
if(details.isEmpty() || (expanded != taskId)) {
reset(viewHolder, taskId); reset(viewHolder, taskId);
if(details.isEmpty() || (extended && expanded != taskId)) {
return; return;
} }
view.setVisibility(View.VISIBLE); view.setVisibility(View.VISIBLE);
@ -455,7 +531,8 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
} }
String string = detailText.toString(); String string = detailText.toString();
if(string.contains("<")) if(string.contains("<"))
view.setText(Html.fromHtml(string.trim().replace("\n", "<br>"), imageGetter, null)); view.setText(Html.fromHtml(string.trim().replace("\n", "<br>"),
detailImageGetter, null));
else else
view.setText(string.trim()); view.setText(string.trim());
Linkify.addLinks(view, Linkify.ALL); Linkify.addLinks(view, Linkify.ALL);
@ -463,7 +540,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
@Override @Override
void reset(ViewHolder viewHolder, long taskId) { void reset(ViewHolder viewHolder, long taskId) {
TextView view = extended ? viewHolder.extendedDetails : viewHolder.details; TextView view = viewHolder.extendedDetails;
view.setVisibility(View.GONE); view.setVisibility(View.GONE);
} }
} }
@ -476,12 +553,18 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
*/ */
public class DecorationManager extends AddOnManager<TaskDecoration> { public class DecorationManager extends AddOnManager<TaskDecoration> {
@Override @Override
Intent createBroadcastIntent(long taskId) { Intent createBroadcastIntent(Task task) {
Intent intent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DECORATIONS); Intent intent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DECORATIONS);
intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
intent.putExtra(BROADCAST_EXTRA_TASK, task);
return intent; return intent;
} }
@Override
public void addNew(long taskId, String addOn, TaskDecoration item) {
super.addNew(taskId, addOn, item);
}
@Override @Override
void draw(ViewHolder viewHolder, long taskId, Collection<TaskDecoration> decorations) { void draw(ViewHolder viewHolder, long taskId, Collection<TaskDecoration> decorations) {
if(decorations == null || viewHolder.task.getId() != taskId) if(decorations == null || viewHolder.task.getId() != taskId)
@ -491,6 +574,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
if(decorations.size() == 0) if(decorations.size() == 0)
return; return;
int i = 0; int i = 0;
boolean colorSet = false; boolean colorSet = false;
viewHolder.decorations = new View[decorations.size()]; viewHolder.decorations = new View[decorations.size()];
@ -520,6 +604,9 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
for(View view : viewHolder.decorations) for(View view : viewHolder.decorations)
viewHolder.taskRow.removeView(view); viewHolder.taskRow.removeView(view);
} }
if(taskId == expanded)
viewHolder.view.setBackgroundResource(R.drawable.list_selector_highlighted);
else
viewHolder.view.setBackgroundResource(android.R.drawable.list_selector_background); viewHolder.view.setBackgroundResource(android.R.drawable.list_selector_background);
} }
} }
@ -531,10 +618,15 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
* *
*/ */
public class TaskActionManager extends AddOnManager<TaskAction> { public class TaskActionManager extends AddOnManager<TaskAction> {
private final LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT, 1f);
@Override @Override
Intent createBroadcastIntent(long taskId) { Intent createBroadcastIntent(Task task) {
Intent intent = new Intent(AstridApiConstants.BROADCAST_REQUEST_ACTIONS); Intent intent = new Intent(AstridApiConstants.BROADCAST_REQUEST_ACTIONS);
intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
return intent; return intent;
} }
@ -543,15 +635,25 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
if(actions == null || viewHolder.task.getId() != taskId) if(actions == null || viewHolder.task.getId() != taskId)
return; return;
reset(viewHolder, taskId); // hack because we know we have > 1 button
if(addOnService.hasPowerPack() && actions.size() == 0)
LinearLayout.LayoutParams params = return;
new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT, 1f);
for(int i = viewHolder.actions.getChildCount(); i < actions.size() + 1; i++) {
Button editButton = new Button(activity); Button editButton = new Button(activity);
editButton.setText(R.string.TAd_actionEditTask); editButton.setLayoutParams(params);
editButton.setOnClickListener(new OnClickListener() { viewHolder.actions.addView(editButton);
}
for(int i = actions.size() + 1; i < viewHolder.actions.getChildCount(); i++) {
viewHolder.actions.getChildAt(i).setVisibility(View.GONE);
}
int i = 0;
Button button = (Button) viewHolder.actions.getChildAt(i++);
button.setText(R.string.TAd_actionEditTask);
button.setVisibility(View.VISIBLE);
button.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View arg0) { public void onClick(View arg0) {
Intent intent = new Intent(activity, TaskEditActivity.class); Intent intent = new Intent(activity, TaskEditActivity.class);
@ -559,17 +661,15 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
activity.startActivityForResult(intent, TaskListActivity.ACTIVITY_EDIT_TASK); activity.startActivityForResult(intent, TaskListActivity.ACTIVITY_EDIT_TASK);
} }
}); });
editButton.setLayoutParams(params);
viewHolder.actions.addView(editButton);
for(TaskAction action : actions) { for(TaskAction action : actions) {
Button view = new Button(activity); button = (Button) viewHolder.actions.getChildAt(i++);
view.setText(action.text); button.setText(action.text);
view.setOnClickListener(new ActionClickListener(action)); button.setVisibility(View.VISIBLE);
view.setLayoutParams(params); button.setOnClickListener(new ActionClickListener(action, viewHolder));
viewHolder.actions.addView(view);
} }
reset(viewHolder, taskId);
} }
@Override @Override
@ -579,7 +679,6 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
return; return;
} }
viewHolder.actions.setVisibility(View.VISIBLE); viewHolder.actions.setVisibility(View.VISIBLE);
viewHolder.actions.removeAllViews();
} }
} }
@ -607,19 +706,25 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
}; };
private final class ActionClickListener implements View.OnClickListener { private final class ActionClickListener implements View.OnClickListener {
TaskAction action; private final TaskAction action;
private final ViewHolder viewHolder;
public ActionClickListener(TaskAction action) { public ActionClickListener(TaskAction action, ViewHolder viewHolder) {
this.action = action; this.action = action;
this.viewHolder = viewHolder;
} }
public void onClick(View v) { public void onClick(View v) {
flushSpecific(viewHolder.task.getId());
try { try {
action.intent.send(); action.intent.send();
} catch (Exception e) { } catch (Exception e) {
exceptionService.displayAndReportError(activity, exceptionService.displayAndReportError(activity,
"Error launching action", e); //$NON-NLS-1$ "Error launching action", e); //$NON-NLS-1$
} }
decorationManager.request(viewHolder);
extendedDetailManager.request(viewHolder);
taskActionManager.request(viewHolder);
} }
}; };
@ -716,7 +821,7 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
abstract public class AddOnManager<TYPE> { abstract public class AddOnManager<TYPE> {
private final Map<Long, HashMap<String, TYPE>> cache = private final Map<Long, HashMap<String, TYPE>> cache =
new SoftHashMap<Long, HashMap<String, TYPE>>(); new HashMap<Long, HashMap<String, TYPE>>();
// --- interface // --- interface
@ -735,13 +840,13 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
// request details // request details
draw(viewHolder, taskId, get(taskId)); draw(viewHolder, taskId, get(taskId));
Intent broadcastIntent = createBroadcastIntent(taskId); Intent broadcastIntent = createBroadcastIntent(viewHolder.task);
activity.sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); activity.sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
return true; return true;
} }
/** creates a broadcast intent for requesting */ /** creates a broadcast intent for requesting */
abstract Intent createBroadcastIntent(long taskId); abstract Intent createBroadcastIntent(Task task);
/** updates the given view */ /** updates the given view */
abstract void draw(ViewHolder viewHolder, long taskId, Collection<TYPE> list); abstract void draw(ViewHolder viewHolder, long taskId, Collection<TYPE> list);
@ -776,6 +881,13 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
cache.clear(); cache.clear();
} }
/**
* Clears single item from cache
*/
public void clearCache(long taskId) {
cache.remove(taskId);
}
// --- internal goodies // --- internal goodies
/** /**

@ -1,26 +0,0 @@
package com.todoroo.astrid.api;
import android.content.Context;
/**
* Internal API for Task Details
*
* @author Tim Su <tim@todoroo.com>
*
*/
public interface DetailExposer {
/**
* @param id
* task id
* @param extended
* whether this request is for extended details (which are
* displayed when user presses a task), or standard (which are
* always displayed)
* @return null if no details, or task details
*/
public String getTaskDetails(Context context, long id, boolean extended);
public String getPluginIdentifier();
}

@ -28,7 +28,7 @@ public class Database extends AbstractDatabase {
* Database version number. This variable must be updated when database * Database version number. This variable must be updated when database
* tables are updated, as it determines whether a database needs updating. * tables are updated, as it determines whether a database needs updating.
*/ */
public static final int VERSION = 4; public static final int VERSION = 5;
/** /**
* Database name (must be unique) * Database name (must be unique)
@ -119,6 +119,10 @@ public class Database extends AbstractDatabase {
append(')'); append(')');
database.execSQL(sql.toString()); database.execSQL(sql.toString());
} }
case 4: {
database.execSQL("ALTER TABLE " + Task.TABLE.name + " ADD " +
Task.DETAILS.accept(visitor, null));
}
return true; return true;
} }

@ -157,6 +157,10 @@ public class TaskDao extends GenericDao<Task> {
return false; return false;
} }
// clear task detail cache
if(values != null && !values.containsKey(Task.DETAILS.name))
values.put(Task.DETAILS.name, ""); //$NON-NLS-1$
if (task.getId() == Task.NO_ID) { if (task.getId() == Task.NO_ID) {
saveSuccessful = createNew(task); saveSuccessful = createNew(task);
} else { } else {
@ -221,10 +225,14 @@ public class TaskDao extends GenericDao<Task> {
* @param skipHooks whether this save occurs as part of a sync * @param skipHooks whether this save occurs as part of a sync
*/ */
private void afterSave(Task task, ContentValues values) { private void afterSave(Task task, ContentValues values) {
if(values != null && values.containsKey(Task.COMPLETION_DATE.name) && task.isCompleted()) if(values != null) {
if(values.containsKey(Task.COMPLETION_DATE.name) && task.isCompleted())
afterComplete(task, values); afterComplete(task, values);
else else if(values.containsKey(Task.DUE_DATE.name) ||
values.containsKey(Task.REMINDER_FLAGS.name) ||
values.containsKey(Task.REMINDER_PERIOD.name))
ReminderService.getInstance().scheduleAlarm(task); ReminderService.getInstance().scheduleAlarm(task);
}
Astrid2TaskProvider.notifyDatabaseModification(); Astrid2TaskProvider.notifyDatabaseModification();
TasksWidget.updateWidgets(ContextManager.getContext()); TasksWidget.updateWidgets(ContextManager.getContext());

@ -79,7 +79,13 @@ public final class Task extends AbstractModel {
public static final LongProperty DELETION_DATE = new LongProperty( public static final LongProperty DELETION_DATE = new LongProperty(
TABLE, "deleted"); TABLE, "deleted");
// --- for migration purposes from astrid 2 (eventually we will want to /** Cached Details Column - built from add-on detail exposers. A null
* value means there is no value in the cache and it needs to be
* refreshed */
public static final StringProperty DETAILS = new StringProperty(
TABLE, "details");
// --- for migration purposes from astrid 2 (eventually we may want to
// move these into the metadata table and treat them as plug-ins // move these into the metadata table and treat them as plug-ins
public static final StringProperty NOTES = new StringProperty( public static final StringProperty NOTES = new StringProperty(
@ -186,6 +192,7 @@ public final class Task extends AbstractModel {
defaultValues.put(NOTES.name, ""); defaultValues.put(NOTES.name, "");
defaultValues.put(FLAGS.name, 0); defaultValues.put(FLAGS.name, 0);
defaultValues.put(TIMER_START.name, 0); defaultValues.put(TIMER_START.name, 0);
defaultValues.put(DETAILS.name, (String)null);
} }
@Override @Override
@ -215,7 +222,7 @@ public final class Task extends AbstractModel {
// --- parcelable helpers // --- parcelable helpers
private static final Creator<Task> CREATOR = new ModelCreator<Task>(Task.class); public static final Creator<Task> CREATOR = new ModelCreator<Task>(Task.class);
@Override @Override
protected Creator<? extends AbstractModel> getCreator() { protected Creator<? extends AbstractModel> getCreator() {
@ -398,6 +405,8 @@ public final class Task extends AbstractModel {
* Checks whether this due date has a due time or only a date * Checks whether this due date has a due time or only a date
*/ */
public boolean hasDueTime() { public boolean hasDueTime() {
if(!hasDueDate())
return false;
return hasDueTime(getValue(Task.DUE_DATE)); return hasDueTime(getValue(Task.DUE_DATE));
} }

@ -42,9 +42,6 @@ public class AddOnService {
/** Astrid Locale package */ /** Astrid Locale package */
public static final String LOCALE_PACKAGE = "com.todoroo.astrid.locale"; public static final String LOCALE_PACKAGE = "com.todoroo.astrid.locale";
/** Astrid Producteev package */
public static final String PRODUCTEEV_PACKAGE = "com.todoroo.astrid.producteev";
/** Astrid Power Pack label */ /** Astrid Power Pack label */
public static final String POWER_PACK_LABEL = "Astrid Power Pack"; public static final String POWER_PACK_LABEL = "Astrid Power Pack";
@ -171,7 +168,7 @@ public class AddOnService {
return true; return true;
if(LOCALE_PACKAGE.equals(packageName)) if(LOCALE_PACKAGE.equals(packageName))
return true; return true;
if(PRODUCTEEV_PACKAGE.equals(packageName)) if(Constants.PACKAGE.equals(packageName))
return true; return true;
Context context = ContextManager.getContext(); Context context = ContextManager.getContext();
@ -234,7 +231,7 @@ public class AddOnService {
list[2] = new AddOn(true, true, "Producteev", null, list[2] = new AddOn(true, true, "Producteev", null,
"Synchronize with Producteev service. Also changes Astrid's importance levels to stars.", "Synchronize with Producteev service. Also changes Astrid's importance levels to stars.",
PRODUCTEEV_PACKAGE, "http://www.producteev.com", Constants.PACKAGE, "http://www.producteev.com",
((BitmapDrawable)r.getDrawable(R.drawable.icon_producteev)).getBitmap()); ((BitmapDrawable)r.getDrawable(R.drawable.icon_producteev)).getBitmap());
list[3] = new AddOn(true, true, "Remember the Milk", null, list[3] = new AddOn(true, true, "Remember the Milk", null,

@ -1,5 +1,6 @@
package com.todoroo.astrid.service; package com.todoroo.astrid.service;
import java.io.File;
import java.util.List; import java.util.List;
import android.Manifest; import android.Manifest;
@ -14,16 +15,22 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.util.Log; import android.util.Log;
import com.flurry.android.FlurryAgent;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.service.ExceptionService.TodorooUncaughtExceptionHandler; import com.todoroo.andlib.service.ExceptionService.TodorooUncaughtExceptionHandler;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.backup.BackupConstants;
import com.todoroo.astrid.backup.BackupService; import com.todoroo.astrid.backup.BackupService;
import com.todoroo.astrid.backup.TasksXmlImporter;
import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.producteev.ProducteevBackgroundService; import com.todoroo.astrid.producteev.ProducteevBackgroundService;
import com.todoroo.astrid.producteev.ProducteevUtilities; import com.todoroo.astrid.producteev.ProducteevUtilities;
import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Preferences; import com.todoroo.astrid.utility.Preferences;
import com.todoroo.astrid.widget.TasksWidget.UpdateService; import com.todoroo.astrid.widget.TasksWidget.UpdateService;
@ -55,6 +62,9 @@ public class StartupService {
@Autowired @Autowired
TaskService taskService; TaskService taskService;
@Autowired
MetadataService metadataService;
@Autowired @Autowired
Database database; Database database;
@ -89,6 +99,8 @@ public class StartupService {
Log.i("astrid", "Astrid Startup. " + latestSetVersion + //$NON-NLS-1$ //$NON-NLS-2$ Log.i("astrid", "Astrid Startup. " + latestSetVersion + //$NON-NLS-1$ //$NON-NLS-2$
" => " + version); //$NON-NLS-1$ " => " + version); //$NON-NLS-1$
databaseRestoreIfEmpty(context);
// invoke upgrade service // invoke upgrade service
boolean justUpgraded = latestSetVersion != version; boolean justUpgraded = latestSetVersion != version;
if(justUpgraded && version > 0) { if(justUpgraded && version > 0) {
@ -101,6 +113,7 @@ public class StartupService {
// perform startup activities in a background thread // perform startup activities in a background thread
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
// start widget updating alarm // start widget updating alarm
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, UpdateService.class); Intent intent = new Intent(context, UpdateService.class);
@ -111,6 +124,15 @@ public class StartupService {
database.openForWriting(); database.openForWriting();
taskService.cleanup(); taskService.cleanup();
// schedule alarms
try {
ReminderService.getInstance().scheduleAllAlarms();
AlarmService.getInstance().scheduleAllAlarms();
} catch (Exception e) {
DependencyInjectionService.getInstance().inject(this);
exceptionService.reportError("reminder-startup", e); //$NON-NLS-1$
}
} }
}).start(); }).start();
@ -129,8 +151,35 @@ public class StartupService {
hasStartedUp = true; hasStartedUp = true;
} }
/**
* If database exists, no tasks but metadata, and a backup file exists, restore it
*/
private void databaseRestoreIfEmpty(Context context) {
try {
if(Preferences.getCurrentVersion() > 135 && !context.getDatabasePath(database.getName()).exists()) {
// we didn't have a database! restore latest file
File directory = BackupConstants.getExportDirectory();
if(!directory.exists())
return;
File[] children = directory.listFiles();
AndroidUtilities.sortFilesByDateDesc(children);
if(children.length > 0) {
FlurryAgent.onStartSession(context, Constants.FLURRY_KEY);
TasksXmlImporter.importTasks(context, children[0].getAbsolutePath(), null);
FlurryAgent.onEvent("lost-tasks-restored"); //$NON-NLS-1$
}
}
} catch (Exception e) {
Log.w("astrid-database-restore", e); //$NON-NLS-1$
}
}
private static final String P_TASK_KILLER_HELP = "taskkiller"; //$NON-NLS-1$ private static final String P_TASK_KILLER_HELP = "taskkiller"; //$NON-NLS-1$
/**
* Show task killer helper
* @param context
*/
private static void showTaskKillerHelp(final Context context) { private static void showTaskKillerHelp(final Context context) {
if(!Preferences.getBoolean(P_TASK_KILLER_HELP, false)) if(!Preferences.getBoolean(P_TASK_KILLER_HELP, false))
return; return;

@ -1,5 +1,7 @@
package com.todoroo.astrid.service; package com.todoroo.astrid.service;
import android.content.ContentValues;
import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.Autowired;
@ -132,6 +134,15 @@ public class TaskService {
} }
} }
/**
* Permanently delete the given task.
*
* @param model
*/
public void purge(long taskId) {
taskDao.delete(taskId);
}
/** /**
* Clean up tasks. Typically called on startup * Clean up tasks. Typically called on startup
*/ */
@ -196,7 +207,7 @@ public class TaskService {
return Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0), return Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0),
DateUtilities.now() + DateUtilities.ONE_WEEK, DateUtilities.now() + DateUtilities.ONE_WEEK,
Task.DUE_DATE) + " + 200000000 * " + Task.DUE_DATE) + " + 200000000 * " +
Task.IMPORTANCE + " + " + Task.COMPLETION_DATE); Task.IMPORTANCE + " + 2*" + Task.COMPLETION_DATE);
} }
/** /**
@ -212,6 +223,18 @@ public class TaskService {
} }
} }
/**
* Clear details cache. Useful if user performs some operation that
* affects details
*
* @return # of affected rows
*/
public int clearDetails() {
ContentValues values = new ContentValues();
values.put(Task.DETAILS.name, (String) null);
return taskDao.updateMultiple(values, Criterion.all);
}
/** /**
* Update database based on selection and values * Update database based on selection and values
* @param selection * @param selection
@ -233,6 +256,21 @@ public class TaskService {
} }
} }
/**
* Count tasks overall
* @param filter
* @return
*/
public int countTasks() {
TodorooCursor<Task> cursor = query(Query.select(Task.ID));
try {
return cursor.getCount();
} finally {
cursor.close();
}
}
/** count tasks in a given filter */
public int countTasks(Filter filter) { public int countTasks(Filter filter) {
String queryTemplate = PermaSql.replacePlaceholders(filter.sqlQuery); String queryTemplate = PermaSql.replacePlaceholders(filter.sqlQuery);
TodorooCursor<Task> cursor = query(Query.select(Task.ID).withQueryTemplate( TodorooCursor<Task> cursor = query(Query.select(Task.ID).withQueryTemplate(

@ -16,6 +16,8 @@ import com.todoroo.astrid.dao.Database;
public final class UpgradeService { public final class UpgradeService {
private static final int V3_2_6 = 153;
private static final int V3_2_5 = 152;
private static final int V3_2_4 = 151; private static final int V3_2_4 = 151;
private static final int V3_2_3 = 150; private static final int V3_2_3 = 150;
private static final int V3_1_0 = 146; private static final int V3_1_0 = 146;
@ -109,11 +111,22 @@ public final class UpgradeService {
"If you liked the old version, you can also go back by " + "If you liked the old version, you can also go back by " +
"<a href='http://bit.ly/oldastrid'>clicking here</a>", "<a href='http://bit.ly/oldastrid'>clicking here</a>",
}); });
if(from > V2_14_4 && from <= V3_2_4) if(from > V3_1_0 && from <= V3_2_6)
newVersionString(changeLog, "3.2.7 (8/25/10)", new String[] {
"Fixed: crazy notifications for overdue tasks! :(",
});
if(from > V3_1_0 && from <= V3_2_5)
newVersionString(changeLog, "3.2.7 (8/24/10)", new String[] {
"RTM: fix for login popping up randomly, not syncing priority",
"Sync: added a 'Sync Now!' button to the menu, moved options to Settings",
"Improvements to notification code to remind you of missed notifications",
"Smoother scrolling of task list once details are loaded",
});
if(from > V3_1_0 && from <= V3_2_4)
newVersionString(changeLog, "3.2.5 (8/18/10)", new String[] { newVersionString(changeLog, "3.2.5 (8/18/10)", new String[] {
"Fix for duplicated tasks created in RTM", "Fix for duplicated tasks created in RTM",
}); });
if(from > V2_14_4 && from <= V3_2_3) if(from > V3_1_0 && from <= V3_2_3)
newVersionString(changeLog, "3.2.5 (8/18/10)", new String[] { newVersionString(changeLog, "3.2.5 (8/18/10)", new String[] {
"Fix for duplicated tasks created in Producteev", "Fix for duplicated tasks created in Producteev",
"Fix for being able to create tasks without title", "Fix for being able to create tasks without title",

@ -40,7 +40,7 @@ public final class Constants {
/** /**
* Upgrade time * Upgrade time
*/ */
public static final Date UPGRADE = new Date(110, 8, 1); public static final Date UPGRADE = new Date(110, 9, 1);
// --- notification id's // --- notification id's
@ -53,4 +53,7 @@ public final class Constants {
/** Notification Manager id for locale */ /** Notification Manager id for locale */
public static final int NOTIFICATION_LOCALE = -3; public static final int NOTIFICATION_LOCALE = -3;
/** Notification Manager id for producteev notifications*/
public static final int NOTIFICATION_PRODUCTEEV_NOTIFICATIONS = -4;
} }

@ -51,12 +51,13 @@ public class TasksWidget extends AppWidgetProvider {
int[] appWidgetIds) { int[] appWidgetIds) {
try { try {
ContextManager.setContext(context);
super.onUpdate(context, appWidgetManager, appWidgetIds); super.onUpdate(context, appWidgetManager, appWidgetIds);
// Start in service to prevent Application Not Responding timeout // Start in service to prevent Application Not Responding timeout
updateWidgets(context); updateWidgets(context);
} catch (SecurityException e) { } catch (Exception e) {
// :( Log.e("astrid-update-widget", "widget update error", e); //$NON-NLS-1$ //$NON-NLS-2$
} }
} }
@ -65,7 +66,7 @@ public class TasksWidget extends AppWidgetProvider {
* @param id * @param id
*/ */
public static void updateWidgets(Context context) { public static void updateWidgets(Context context) {
context.startService(new Intent(ContextManager.getContext(), context.startService(new Intent(context,
TasksWidget.UpdateService.class)); TasksWidget.UpdateService.class));
} }
@ -146,7 +147,7 @@ public class TasksWidget extends AppWidgetProvider {
filter.sqlQuery, flags, sort) + " LIMIT " + numberOfTasks; filter.sqlQuery, flags, sort) + " LIMIT " + numberOfTasks;
database.openForReading(); database.openForReading();
cursor = taskService.fetchFiltered(query, null, Task.TITLE, Task.DUE_DATE); cursor = taskService.fetchFiltered(query, null, Task.ID, Task.TITLE, Task.DUE_DATE, Task.COMPLETION_DATE);
Task task = new Task(); Task task = new Task();
for (int i = 0; i < cursor.getCount() && i < numberOfTasks; i++) { for (int i = 0; i < cursor.getCount() && i < numberOfTasks; i++) {
cursor.moveToPosition(i); cursor.moveToPosition(i);
@ -156,7 +157,10 @@ public class TasksWidget extends AppWidgetProvider {
int textColor = Color.WHITE; int textColor = Color.WHITE;
textContent = task.getValue(Task.TITLE); textContent = task.getValue(Task.TITLE);
if(task.hasDueDate() && task.getValue(Task.DUE_DATE) < DateUtilities.now())
if(task.isCompleted())
textColor = context.getResources().getColor(R.color.task_list_done);
else if(task.hasDueDate() && task.getValue(Task.DUE_DATE) < DateUtilities.now())
textColor = context.getResources().getColor(R.color.task_list_overdue); textColor = context.getResources().getColor(R.color.task_list_overdue);
if(i > 0) if(i > 0)
@ -178,7 +182,7 @@ public class TasksWidget extends AppWidgetProvider {
} }
Intent listIntent = new Intent(context, TaskListActivity.class); Intent listIntent = new Intent(context, TaskListActivity.class);
listIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); listIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
if(filter != null) { if(filter != null) {
listIntent.putExtra(TaskListActivity.TOKEN_FILTER, filter); listIntent.putExtra(TaskListActivity.TOKEN_FILTER, filter);
listIntent.setType(filter.sqlQuery); listIntent.setType(filter.sqlQuery);
@ -188,7 +192,7 @@ public class TasksWidget extends AppWidgetProvider {
views.setOnClickPendingIntent(R.id.taskbody, pendingIntent); views.setOnClickPendingIntent(R.id.taskbody, pendingIntent);
Intent editIntent = new Intent(context, TaskEditActivity.class); Intent editIntent = new Intent(context, TaskEditActivity.class);
editIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); editIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
if(filter != null && filter.valuesForNewTasks != null) { if(filter != null && filter.valuesForNewTasks != null) {
String values = AndroidUtilities.contentValuesToSerializedString(filter.valuesForNewTasks); String values = AndroidUtilities.contentValuesToSerializedString(filter.valuesForNewTasks);
editIntent.putExtra(TaskEditActivity.TOKEN_VALUES, values); editIntent.putExtra(TaskEditActivity.TOKEN_VALUES, values);

Loading…
Cancel
Save