diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml index 89957c65d..cef991eb2 100644 --- a/astrid/AndroidManifest.xml +++ b/astrid/AndroidManifest.xml @@ -207,6 +207,14 @@ + + + + + + + + * */ -public final class EditOperation implements Parcelable { - - /** - * Plugin Id - */ - public final String plugin; +public class TaskAction implements Parcelable { /** * Label @@ -33,15 +28,13 @@ public final class EditOperation implements Parcelable { /** * Create an EditOperation object * - * @param plugin - * {@link Addon} identifier that encompasses object * @param text * label to display * @param intent * intent to invoke. {@link EXTRAS_TASK_ID} will be passed */ - public EditOperation(String plugin, String text, Intent intent) { - this.plugin = plugin; + public TaskAction(String text, Intent intent) { + super(); this.text = text; this.intent = intent; } @@ -59,7 +52,6 @@ public final class EditOperation implements Parcelable { * {@inheritDoc} */ public void writeToParcel(Parcel dest, int flags) { - dest.writeString(plugin); dest.writeString(text); dest.writeParcelable(intent, 0); } @@ -67,20 +59,20 @@ public final class EditOperation implements Parcelable { /** * Parcelable creator */ - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { /** * {@inheritDoc} */ - public EditOperation createFromParcel(Parcel source) { - return new EditOperation(source.readString(), source.readString(), - (Intent)source.readParcelable(Intent.class.getClassLoader())); + public TaskAction createFromParcel(Parcel source) { + return new TaskAction(source.readString(), (Intent)source.readParcelable( + Intent.class.getClassLoader())); } /** * {@inheritDoc} */ - public EditOperation[] newArray(int size) { - return new EditOperation[size]; + public TaskAction[] newArray(int size) { + return new TaskAction[size]; }; }; diff --git a/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java b/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java index a12e08394..770bf51da 100644 --- a/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java +++ b/astrid/api-src/com/todoroo/astrid/api/TaskContainer.java @@ -6,9 +6,10 @@ import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Task; /** - * Container class for tasks. Synchronization Providers can subclass - * this class if desired. + * Container class for transmitting tasks and including local and remote + * metadata. Synchronization Providers can subclass this class if desired. * + * @see {@link SynchronizationProvider} * @author Tim Su * */ diff --git a/astrid/api-src/com/todoroo/astrid/api/TaskDecoration.java b/astrid/api-src/com/todoroo/astrid/api/TaskDecoration.java new file mode 100644 index 000000000..91367fd53 --- /dev/null +++ b/astrid/api-src/com/todoroo/astrid/api/TaskDecoration.java @@ -0,0 +1,101 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.api; + +import android.os.Parcel; +import android.os.Parcelable; +import android.widget.RemoteViews; +import android.widget.RemoteViews.RemoteView; + +/** + * Represents a line of text displayed in the Task List + * + * @author Tim Su + * + */ +public final class TaskDecoration implements Parcelable { + + /** + * Place decoration between completion box and task title + */ + public static final int POSITION_LEFT = 0; + + /** + * Place decoration between task title and importance bar + */ + public static final int POSITION_RIGHT = 1; + + /** + * {@link RemoteView} decoration + */ + public RemoteViews decoration = null; + + /** + * Decoration update interval (minimum of 1000 millis), 0 to never update + */ + public long updateInterval = 0; + + /** + * Decoration position + */ + public int position = POSITION_LEFT; + + /** + * Decorated task background color. 0 is default + */ + public int color = 0; + + /** + * Creates a TaskDetail object + * @param text + * text to display + * @param color + * color to use for text. Use 0 for default color + */ + public TaskDecoration(RemoteViews decoration, int position, int color) { + this.decoration = decoration; + this.position = position; + this.color = color; + } + + // --- parcelable helpers + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(decoration, 0); + dest.writeInt(position); + dest.writeInt(color); + } + + /** + * Parcelable creator + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * {@inheritDoc} + */ + public TaskDecoration createFromParcel(Parcel source) { + return new TaskDecoration((RemoteViews)source.readParcelable( + RemoteViews.class.getClassLoader()), + source.readInt(), source.readInt()); + } + + /** + * {@inheritDoc} + */ + public TaskDecoration[] newArray(int size) { + return new TaskDecoration[size]; + }; + }; + +} diff --git a/astrid/api-src/com/todoroo/astrid/api/TaskDetail.java b/astrid/api-src/com/todoroo/astrid/api/TaskDetail.java deleted file mode 100644 index 4d24501e8..000000000 --- a/astrid/api-src/com/todoroo/astrid/api/TaskDetail.java +++ /dev/null @@ -1,84 +0,0 @@ -/** - * See the file "LICENSE" for the full license governing this code. - */ -package com.todoroo.astrid.api; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Represents a line of text displayed in the Task List - * - * @author Tim Su - * - */ -public final class TaskDetail implements Parcelable { - - /** - * Text of detail - */ - public String text = null; - - /** - * Color to use for text. 0 is default - */ - public int color = 0; - - /** - * Creates a TaskDetail object - * @param text - * text to display - * @param color - * color to use for text. Use 0 for default color - */ - public TaskDetail(String text, int color) { - this.text = text; - this.color = color; - } - - /** - * Convenience constructor to make a TaskDetail with default color - * @param text - * text to display - */ - public TaskDetail(String text) { - this(text, 0); - } - - // --- parcelable helpers - - /** - * {@inheritDoc} - */ - public int describeContents() { - return 0; - } - - /** - * {@inheritDoc} - */ - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(text); - dest.writeInt(color); - } - - /** - * Parcelable creator - */ - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - /** - * {@inheritDoc} - */ - public TaskDetail createFromParcel(Parcel source) { - return new TaskDetail(source.readString(), source.readInt()); - } - - /** - * {@inheritDoc} - */ - public TaskDetail[] newArray(int size) { - return new TaskDetail[size]; - }; - }; - -} diff --git a/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java b/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java index 894f87467..0a76591ec 100644 --- a/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java @@ -214,12 +214,13 @@ public class AndroidUtilities { /** * Copy a file from one place to another + * * @param in * @param out * @throws Exception */ public static void copyFile(File in, File out) throws Exception { - FileInputStream fis = new FileInputStream(in); + FileInputStream fis = new FileInputStream(in); FileOutputStream fos = new FileOutputStream(out); try { byte[] buf = new byte[1024]; @@ -227,13 +228,11 @@ public class AndroidUtilities { while ((i = fis.read(buf)) != -1) { fos.write(buf, 0, i); } - } - catch (Exception e) { + } catch (Exception e) { throw e; + } finally { + fis.close(); + fos.close(); } - finally { - if (fis != null) fis.close(); - if (fos != null) fos.close(); - } - } + } } diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CorePlugin.java b/astrid/plugin-src/com/todoroo/astrid/core/CorePlugin.java index c76847423..7f8848108 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CorePlugin.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CorePlugin.java @@ -12,9 +12,10 @@ public class CorePlugin extends BroadcastReceiver { static final String IDENTIFIER = "core"; //$NON-NLS-1$ @Override + @SuppressWarnings("nls") public void onReceive(Context context, Intent intent) { Addon plugin = new Addon(IDENTIFIER, "Core Filters", "Todoroo", - "Provides 'Inbox' and 'All Tasks' Filters"); + "Provides 'Inbox', 'Search', and 'More...' Filters"); Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_ADDONS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, plugin); diff --git a/astrid/plugin-src/com/todoroo/astrid/notes/NoteDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/notes/NoteDetailExposer.java index f5ef99e88..55a73f62a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/notes/NoteDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/notes/NoteDetailExposer.java @@ -11,12 +11,11 @@ import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.DetailExposer; -import com.todoroo.astrid.api.TaskDetail; import com.todoroo.astrid.model.Task; import com.todoroo.astrid.service.TaskService; /** - * Exposes {@link TaskDetail} for tags, i.e. "Tags: frogs, animals" + * Exposes Task Detail for notes * * @author Tim Su * @@ -35,7 +34,8 @@ public class NoteDetailExposer extends BroadcastReceiver implements DetailExpose if(taskId == -1) return; - TaskDetail taskDetail = getTaskDetails(context, taskId); + boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); + String taskDetail = getTaskDetails(context, taskId, extended); if(taskDetail == null) return; @@ -43,12 +43,16 @@ public class NoteDetailExposer extends BroadcastReceiver implements DetailExpose Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, NotesPlugin.IDENTIFIER); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); } @Override - public TaskDetail getTaskDetails(Context context, long id) { + public String getTaskDetails(Context context, long id, boolean extended) { + if(!extended) + return null; + synchronized(NoteDetailExposer.class) { if(staticTaskService == null) { DependencyInjectionService.getInstance().inject(this); @@ -65,7 +69,7 @@ public class NoteDetailExposer extends BroadcastReceiver implements DetailExpose if(notes.length() == 0) return null; - return new TaskDetail(notes); + return notes; } } diff --git a/astrid/plugin-src/com/todoroo/astrid/notes/NotesPlugin.java b/astrid/plugin-src/com/todoroo/astrid/notes/NotesPlugin.java index 2948c27b8..88038e84f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/notes/NotesPlugin.java +++ b/astrid/plugin-src/com/todoroo/astrid/notes/NotesPlugin.java @@ -12,6 +12,7 @@ public class NotesPlugin extends BroadcastReceiver { static final String IDENTIFIER = "notes"; //$NON-NLS-1$ @Override + @SuppressWarnings("nls") public void onReceive(Context context, Intent intent) { Addon plugin = new Addon(IDENTIFIER, "Notes", "Todoroo", "Lets you add and view notes for a task."); diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java index 894643997..be7b51452 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java @@ -20,12 +20,11 @@ import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.DetailExposer; -import com.todoroo.astrid.api.TaskDetail; import com.todoroo.astrid.model.Task; import com.todoroo.astrid.service.TaskService; /** - * Exposes {@link TaskDetail} for tags, i.e. "Tags: frogs, animals" + * Exposes Task Detail for repeats, i.e. "Repeats every 2 days" * * @author Tim Su * @@ -44,7 +43,8 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo if(taskId == -1) return; - TaskDetail taskDetail = getTaskDetails(context, taskId); + boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); + String taskDetail = getTaskDetails(context, taskId, extended); if(taskDetail == null) return; @@ -52,11 +52,15 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, RepeatsPlugin.IDENTIFIER); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); } - public TaskDetail getTaskDetails(Context context, long id) { + public String getTaskDetails(Context context, long id, boolean extended) { + if(extended) + return null; + synchronized(RepeatDetailExposer.class) { if(staticTaskService == null) { DependencyInjectionService.getInstance().inject(this); @@ -126,7 +130,7 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo else detail = context.getString(R.string.repeat_detail_duedate, interval); - return new TaskDetail(detail); + return detail; } return null; } diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkDetailExposer.java index 60d1ee278..db816776d 100644 --- a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkDetailExposer.java @@ -12,17 +12,16 @@ import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.astrid.adapter.TaskAdapter; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.DetailExposer; -import com.todoroo.astrid.api.TaskDetail; import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.rmilk.data.MilkDataService; import com.todoroo.astrid.rmilk.data.MilkNote; import com.todoroo.astrid.rmilk.data.MilkTask; /** - * Exposes {@link TaskDetail}s for Remember the Milk: + * Exposes Task Details for Remember the Milk: * - RTM list * - RTM repeat information - * - whether task has been changed + * - RTM notes * * @author Tim Su * @@ -39,49 +38,54 @@ public class MilkDetailExposer extends BroadcastReceiver implements DetailExpose if(taskId == -1) return; - TaskDetail taskDetail = getTaskDetails(context, taskId); + boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); + String taskDetail = getTaskDetails(context, taskId, extended); if(taskDetail == null) return; Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, Utilities.IDENTIFIER); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); } @Override - public TaskDetail getTaskDetails(Context context, long id) { + public String getTaskDetails(Context context, long id, boolean extended) { Metadata metadata = MilkDataService.getInstance().getTaskMetadata(id); if(metadata == null) return null; StringBuilder builder = new StringBuilder(); - long listId = metadata.getValue(MilkTask.LIST_ID); - if(listId > 0) { - builder.append(context.getString(R.string.rmilk_TLA_list, - MilkDataService.getInstance().getListName(listId))).append(TaskAdapter.DETAIL_SEPARATOR); - } - int repeat = metadata.getValue(MilkTask.REPEATING); - if(repeat != 0) { - builder.append(context.getString(R.string.rmilk_TLA_repeat)).append(TaskAdapter.DETAIL_SEPARATOR); - } + if(!extended) { + long listId = metadata.getValue(MilkTask.LIST_ID); + if(listId > 0) { + builder.append(context.getString(R.string.rmilk_TLA_list, + MilkDataService.getInstance().getListName(listId))).append(TaskAdapter.DETAIL_SEPARATOR); + } - TodorooCursor notesCursor = MilkDataService.getInstance().getTaskNotesCursor(id); - try { - for(notesCursor.moveToFirst(); !notesCursor.isAfterLast(); notesCursor.moveToNext()) { - metadata.readFromCursor(notesCursor); - builder.append(MilkNote.toTaskDetail(metadata)).append(TaskAdapter.DETAIL_SEPARATOR); + int repeat = metadata.getValue(MilkTask.REPEATING); + if(repeat != 0) { + builder.append(context.getString(R.string.rmilk_TLA_repeat)).append(TaskAdapter.DETAIL_SEPARATOR); + } + } else { + TodorooCursor notesCursor = MilkDataService.getInstance().getTaskNotesCursor(id); + try { + for(notesCursor.moveToFirst(); !notesCursor.isAfterLast(); notesCursor.moveToNext()) { + metadata.readFromCursor(notesCursor); + builder.append(MilkNote.toTaskDetail(metadata)).append(TaskAdapter.DETAIL_SEPARATOR); + } + } finally { + notesCursor.close(); } - } finally { - notesCursor.close(); } if(builder.length() == 0) return null; String result = builder.toString(); - return new TaskDetail(result.substring(0, result.length() - 3)); + return result.substring(0, result.length() - TaskAdapter.DETAIL_SEPARATOR.length()); } } diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagDetailExposer.java index 16a7b4668..a91ae0614 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/TagDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagDetailExposer.java @@ -10,10 +10,9 @@ import android.content.Intent; import com.timsu.astrid.R; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.DetailExposer; -import com.todoroo.astrid.api.TaskDetail; /** - * Exposes {@link TaskDetail} for tags, i.e. "Tags: frogs, animals" + * Exposes Task Detail for tags, i.e. "Tags: frogs, animals" * * @author Tim Su * @@ -27,7 +26,8 @@ public class TagDetailExposer extends BroadcastReceiver implements DetailExposer if(taskId == -1) return; - TaskDetail taskDetail = getTaskDetails(context, taskId); + boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); + String taskDetail = getTaskDetails(context, taskId, extended); if(taskDetail == null) return; @@ -35,18 +35,21 @@ public class TagDetailExposer extends BroadcastReceiver implements DetailExposer Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, TagsPlugin.IDENTIFIER); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); } @Override - public TaskDetail getTaskDetails(Context context, long id) { - String tagList = TagService.getInstance().getTagsAsString(id); + public String getTaskDetails(Context context, long id, boolean extended) { + if(extended) + return null; + String tagList = TagService.getInstance().getTagsAsString(id); if(tagList.length() == 0) return null; - return new TaskDetail(context.getString(R.string.tag_TLA_detail, tagList)); + return context.getString(R.string.tag_TLA_detail, tagList); } } diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagsPlugin.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagsPlugin.java index 42a0e3617..7a3f168fd 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/TagsPlugin.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagsPlugin.java @@ -9,8 +9,9 @@ import com.todoroo.astrid.api.AstridApiConstants; public class TagsPlugin extends BroadcastReceiver { - static final String IDENTIFIER = "tags"; + static final String IDENTIFIER = "tags"; //$NON-NLS-1$ + @SuppressWarnings("nls") @Override public void onReceive(Context context, Intent intent) { Addon plugin = new Addon(IDENTIFIER, "Tags", "Todoroo", diff --git a/astrid/plugin-src/com/todoroo/astrid/timers/TimerDecorationExposer.java b/astrid/plugin-src/com/todoroo/astrid/timers/TimerDecorationExposer.java new file mode 100644 index 000000000..cf8ebd870 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/timers/TimerDecorationExposer.java @@ -0,0 +1,116 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.timers; + +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.widget.RemoteViews; + +import com.timsu.astrid.R; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.TaskDecoration; +import com.todoroo.astrid.service.TaskService; + +/** + * Exposes {@link TaskDecoration} for timers + * + * @author Tim Su + * + */ +public class TimerDecorationExposer extends BroadcastReceiver { + + private static final int TASK_BG_COLOR = Color.argb(200, 220, 50, 0); + + private static TaskService staticTaskService; + private static HashMap decorations = + new HashMap(); + private static HashMap timers = + new HashMap(); + + @Autowired + private TaskService taskService; + + private static class TimerTimerTask extends TimerTask { + int time; + RemoteViews remoteViews; + + public TimerTimerTask(int time, RemoteViews remoteViews) { + super(); + this.time = time; + this.remoteViews = remoteViews; + } + + @Override + public void run() { + time++; + int seconds = time % 60; + int minutes = time / 60; + if(minutes > 59) { + int hours = minutes / 60; + minutes %= 60; + remoteViews.setTextViewText(R.id.timer, + String.format("%02d:%02d:%02d", //$NON-NLS-1$ + hours, minutes, seconds)); + } else { + remoteViews.setTextViewText(R.id.timer, + String.format("%02d:%02d", //$NON-NLS-1$ + minutes, seconds)); + } + } + } + + public void removeFromCache(long taskId) { + decorations.remove(taskId); + timers.get(taskId).cancel(); + timers.remove(taskId); + } + + @Override + public void onReceive(Context context, Intent intent) { + long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + synchronized(TimerDecorationExposer.class) { + if(staticTaskService == null) { + DependencyInjectionService.getInstance().inject(this); + staticTaskService = taskService; + } else { + taskService = staticTaskService; + } + } + + TaskDecoration decoration; + + if(!decorations.containsKey(taskId)) { + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), + R.layout.timer_decoration); + decoration = new TaskDecoration(remoteViews, + TaskDecoration.POSITION_LEFT, TASK_BG_COLOR); + decorations.put(taskId, decoration); + Timer timer = new Timer(); + timers.put(taskId, timer); + timer.scheduleAtFixedRate(new TimerTimerTask(0, + remoteViews), 0, 1000L); + } else { + decoration = decorations.get(taskId); + } + + // transmit + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DECORATIONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, TimerPlugin.IDENTIFIER); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, decoration); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java b/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java new file mode 100644 index 000000000..45bf1e168 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java @@ -0,0 +1,25 @@ +package com.todoroo.astrid.timers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.todoroo.astrid.api.Addon; +import com.todoroo.astrid.api.AstridApiConstants; + +public class TimerPlugin extends BroadcastReceiver { + + static final String IDENTIFIER = "timer"; //$NON-NLS-1$ + + @Override + @SuppressWarnings("nls") + public void onReceive(Context context, Intent intent) { + Addon plugin = new Addon(IDENTIFIER, "Timer", "Todoroo", + "Lets you time how long it takes to complete tasks."); + + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_ADDONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, plugin); + context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + } + +} diff --git a/astrid/res/drawable/timers_decoration.png b/astrid/res/drawable/timers_decoration.png new file mode 100644 index 000000000..18b7c6781 Binary files /dev/null and b/astrid/res/drawable/timers_decoration.png differ diff --git a/astrid/res/layout/task_adapter_row.xml b/astrid/res/layout/task_adapter_row.xml index 20549d7c8..bad4d561a 100644 --- a/astrid/res/layout/task_adapter_row.xml +++ b/astrid/res/layout/task_adapter_row.xml @@ -10,7 +10,7 @@ android:paddingRight="6dip" android:orientation="vertical"> - + + + + + + + + + + + diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java index bb35ff5ce..ce45edc46 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java @@ -52,7 +52,7 @@ import com.todoroo.astrid.adapter.TaskAdapter; import com.todoroo.astrid.adapter.TaskAdapter.ViewHolder; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.Filter; -import com.todoroo.astrid.api.TaskDetail; +import com.todoroo.astrid.api.TaskDecoration; import com.todoroo.astrid.core.CoreFilterExposer; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.TaskDao.TaskCriteria; @@ -378,6 +378,8 @@ public class TaskListActivity extends ListActivity implements OnScrollListener { super.onResume(); registerReceiver(detailReceiver, new IntentFilter(AstridApiConstants.BROADCAST_SEND_DETAILS)); + registerReceiver(detailReceiver, + new IntentFilter(AstridApiConstants.BROADCAST_SEND_DECORATIONS)); } @Override @@ -387,7 +389,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener { } /** - * Receiver which receives intents to add items to the filter list + * Receiver which receives detail or decoration intents * * @author Tim Su * @@ -398,8 +400,17 @@ public class TaskListActivity extends ListActivity implements OnScrollListener { try { Bundle extras = intent.getExtras(); long taskId = extras.getLong(AstridApiConstants.EXTRAS_TASK_ID); - TaskDetail detail = extras.getParcelable(AstridApiConstants.EXTRAS_RESPONSE); - taskAdapter.addDetails(getListView(), taskId, detail); + + if(AstridApiConstants.BROADCAST_SEND_DECORATIONS.equals(intent.getAction())) { + TaskDecoration deco = extras.getParcelable(AstridApiConstants.EXTRAS_RESPONSE); + taskAdapter.addDecorations(getListView(), taskId, + deco); + } else { + String detail = extras.getString(AstridApiConstants.EXTRAS_RESPONSE); + taskAdapter.addDetails(getListView(), taskId, + extras.getBoolean(AstridApiConstants.EXTRAS_EXTENDED), + detail); + } } catch (Exception e) { exceptionService.reportError("receive-detail-" + //$NON-NLS-1$ intent.getStringExtra(AstridApiConstants.EXTRAS_ADDON), e); @@ -463,7 +474,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener { if(requery) { taskCursor.requery(); - taskAdapter.flushDetailCache(); + taskAdapter.flushCaches(); taskAdapter.notifyDataSetChanged(); } startManagingCursor(taskCursor); diff --git a/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java b/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java index c806340c9..b1ba04bf0 100644 --- a/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java +++ b/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java @@ -1,11 +1,15 @@ package com.todoroo.astrid.adapter; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; -import android.app.Activity; +import android.app.ListActivity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -19,11 +23,11 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.CheckBox; import android.widget.CursorAdapter; import android.widget.LinearLayout; -import android.widget.LinearLayout.LayoutParams; import android.widget.ListView; import android.widget.TextView; @@ -40,7 +44,7 @@ import com.todoroo.astrid.activity.TaskEditActivity; import com.todoroo.astrid.activity.TaskListActivity; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.DetailExposer; -import com.todoroo.astrid.api.TaskDetail; +import com.todoroo.astrid.api.TaskDecoration; import com.todoroo.astrid.model.Task; import com.todoroo.astrid.notes.NoteDetailExposer; import com.todoroo.astrid.repeats.RepeatDetailExposer; @@ -97,15 +101,19 @@ public class TaskAdapter extends CursorAdapter { @Autowired DialogUtilities dialogUtilities; - protected final Activity activity; + protected final ListActivity activity; protected final HashMap completedItems; public boolean isFling = false; private final int resource; private final LayoutInflater inflater; protected OnCompletedTaskListener onCompletedTaskListener = null; private int fontSize; - private static final SoftHashMap> detailCache = - new SoftHashMap>(); + + // --- task detail and decoration soft caches + + private static final TaskCache detailCache = new TaskCache(); + private static final TaskCache extendedDetailCache = new TaskCache(); + private static final TaskCache decorationCache = new TaskCache(); /** * Constructor @@ -120,7 +128,7 @@ public class TaskAdapter extends CursorAdapter { * @param onCompletedTaskListener * task listener. can be null */ - public TaskAdapter(Activity activity, int resource, + public TaskAdapter(ListActivity activity, int resource, TodorooCursor c, boolean autoRequery, OnCompletedTaskListener onCompletedTaskListener) { super(activity, c, autoRequery); @@ -155,7 +163,9 @@ public class TaskAdapter extends CursorAdapter { viewHolder.completeBox = (CheckBox)view.findViewById(R.id.completeBox); viewHolder.dueDate = (TextView)view.findViewById(R.id.dueDate); viewHolder.details = (TextView)view.findViewById(R.id.details); + viewHolder.extendedDetails = (TextView)view.findViewById(R.id.extendedDetails); viewHolder.actions = (LinearLayout)view.findViewById(R.id.actions); + viewHolder.taskRow = (LinearLayout)view.findViewById(R.id.task_row); viewHolder.importance = (View)view.findViewById(R.id.importance); view.setTag(viewHolder); @@ -204,9 +214,13 @@ public class TaskAdapter extends CursorAdapter { public CheckBox completeBox; public TextView dueDate; public TextView details; + public TextView extendedDetails; public View importance; public LinearLayout actions; + public LinearLayout taskRow; public boolean expanded; + + public View[] decorations; } /** Helper method to set the contents and visibility of each field */ @@ -251,7 +265,6 @@ public class TaskAdapter extends CursorAdapter { dueDateView.setText(r.getString(R.string.TAd_completed, dateValue)); dueDateView.setTextAppearance(activity, R.style.TextAppearance_TAd_ItemDetails); setVisibility(dueDateView); - } else { dueDateView.setVisibility(View.GONE); } @@ -265,43 +278,63 @@ public class TaskAdapter extends CursorAdapter { completeBox.setChecked(task.isCompleted()); } - // task details - send out a request for it (only if not fling) - viewHolder.details.setText(""); //$NON-NLS-1$ - if(!isFling) { - retrieveDetails(viewHolder); - } - - // importance bar - must be set at end when view height is determined + // importance bar final View importanceView = viewHolder.importance; { int value = task.getValue(Task.IMPORTANCE); importanceView.setBackgroundColor(IMPORTANCE_COLORS[value]); } + + // details and decorations + viewHolder.details.setText(""); //$NON-NLS-1$ + if(viewHolder.decorations != null) { + for(View decoration: viewHolder.decorations) + viewHolder.taskRow.removeView(decoration); + } + viewHolder.taskRow.setBackgroundDrawable(null); + if(!isFling) { + // task details - send out a request for it + retrieveDetails(viewHolder, false); + + // task decoration - send out a request for it + retrieveDecorations(viewHolder); + } } - // --- task details + protected TaskRowListener listener = new TaskRowListener(); + /** + * Set listeners for this view. This is called once per view when it is + * created. + */ + private void addListeners(final View container) { + // check box listener + final CheckBox completeBox = ((CheckBox)container.findViewById(R.id.completeBox)); + completeBox.setOnClickListener(completeBoxListener); + + // context menu listener + container.setOnCreateContextMenuListener(listener); - private void retrieveDetails(final ViewHolder viewHolder) { - final long taskId = viewHolder.task.getId(); + // tap listener + container.setOnClickListener(listener); + } - // check the cache - boolean inCache = false; - final LinkedHashSet details; - synchronized(detailCache) { - if(detailCache.containsKey(taskId)) - inCache = true; - else - detailCache.put(taskId, new LinkedHashSet()); - details = detailCache.get(taskId); - } + /* ====================================================================== + * ============================================================== add-ons + * ====================================================================== */ - if(inCache) { - spanifyAndAdd(viewHolder.details, details); + private void retrieveDetails(final ViewHolder viewHolder, final boolean extended) { + final long taskId = viewHolder.task.getId(); + + final TaskCache cache = extended ? extendedDetailCache : detailCache; + ArrayList list = cache.initialize(taskId); + if(list != null) { + spanifyAndAdd(viewHolder, extended, list); return; } // request details Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DETAILS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_EXTENDED, extended); activity.sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); // load internal details @@ -309,35 +342,40 @@ public class TaskAdapter extends CursorAdapter { @Override public void run() { for(DetailExposer exposer : EXPOSERS) { - if(Thread.interrupted()) - return; - - final TaskDetail detail = exposer.getTaskDetails(activity, taskId); - if(detail == null || details.contains(detail)) - continue; - - details.add(detail); - if(taskId != viewHolder.task.getId()) + final String detail = exposer.getTaskDetails(activity, + taskId, extended); + if(detail == null) continue; - - activity.runOnUiThread(new Runnable() { - public void run() { - spanifyAndAdd(viewHolder.details, details); - } - }); + final ArrayList cacheList = cache.addIfNotExists(taskId, detail); + if(cacheList != null) { + if(taskId != viewHolder.task.getId()) + continue; + activity.runOnUiThread(new Runnable() { + public void run() { + spanifyAndAdd(viewHolder, extended, cacheList); + } + }); + } } }; }.start(); } @SuppressWarnings("nls") - private void spanifyAndAdd(TextView view, LinkedHashSet details) { + private void spanifyAndAdd(ViewHolder viewHolder, boolean extended, ArrayList details) { + if(details == null) + return; + TextView view = extended ? viewHolder.extendedDetails : viewHolder.details; view.setVisibility(details.size() > 0 ? View.VISIBLE : View.GONE); - if(details.size() == 0) + if(details.size() == 0 || (extended && !viewHolder.expanded)) { + view.setVisibility(View.GONE); return; + } else { + view.setVisibility(View.VISIBLE); + } StringBuilder detailText = new StringBuilder(); - for(Iterator iterator = details.iterator(); iterator.hasNext(); ) { - detailText.append(iterator.next().text); + for(Iterator iterator = details.iterator(); iterator.hasNext(); ) { + detailText.append(iterator.next()); if(iterator.hasNext()) detailText.append(DETAIL_SEPARATOR); } @@ -351,14 +389,10 @@ public class TaskAdapter extends CursorAdapter { /** * Called to tell the cache to be cleared */ - public void flushDetailCache() { + public void flushCaches() { detailCache.clear(); - } - - @Override - public void notifyDataSetChanged() { - super.notifyDataSetChanged(); - fontSize = Preferences.getIntegerFromString(R.string.p_fontSize); + extendedDetailCache.clear(); + decorationCache.clear(); } /** @@ -366,23 +400,24 @@ public class TaskAdapter extends CursorAdapter { * * @param taskId */ - public synchronized void addDetails(ListView list, long taskId, TaskDetail detail) { + public synchronized void addDetails(ListView listView, long taskId, + boolean extended, String detail) { if(detail == null) return; - LinkedHashSet details = detailCache.get(taskId); - if(details.contains(detail)) - return; - details.add(detail); - - // update view if it is visible - int length = list.getChildCount(); - for(int i = 0; i < length; i++) { - ViewHolder viewHolder = (ViewHolder) list.getChildAt(i).getTag(); - if(viewHolder == null || viewHolder.task.getId() != taskId) - continue; - spanifyAndAdd(viewHolder.details, details); - break; + final TaskCache cache = extended ? extendedDetailCache : detailCache; + + ArrayList cacheList = cache.addIfNotExists(taskId, detail); + if(cacheList != null) { + // update view if it is visible + int length = listView.getChildCount(); + for(int i = 0; i < length; i++) { + ViewHolder viewHolder = (ViewHolder) listView.getChildAt(i).getTag(); + if(viewHolder == null || viewHolder.task.getId() != taskId) + continue; + spanifyAndAdd(viewHolder, extended, cacheList); + break; + } } } @@ -397,29 +432,120 @@ public class TaskAdapter extends CursorAdapter { } }; - protected ContextMenuListener listener = new ContextMenuListener(); + private void retrieveDecorations(final ViewHolder viewHolder) { + final long taskId = viewHolder.task.getId(); + + ArrayList list = decorationCache.initialize(taskId); + if(list != null) { + decorate(viewHolder, list); + return; + } + + // request details + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_DECORATIONS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId); + activity.sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); + + if(timer == null) { + timer = new Timer(); + timer.scheduleAtFixedRate(decorationUpdater, 0, 1000L); + } + } + + private Timer timer = null; + /** - * Set listeners for this view. This is called once per view when it is - * created. + * Task to update decorations every second */ - private void addListeners(final View container) { - // check box listener - final CheckBox completeBox = ((CheckBox)container.findViewById(R.id.completeBox)); - completeBox.setOnClickListener(completeBoxListener); + private final TimerTask decorationUpdater = new TimerTask() { + @Override + public void run() { + ListView listView = activity.getListView(); + int length = listView.getChildCount(); + for(int i = 0; i < length; i++) { + ViewHolder viewHolder = (ViewHolder) listView.getChildAt(i).getTag(); + ArrayList list = decorationCache.get(viewHolder.task.getId()); + if(list == null) + continue; + + for(int j = 0; j < list.size(); j++) { + final TaskDecoration decoration = list.get(j); + if(decoration.decoration == null) + continue; + final View view = viewHolder.decorations[j]; + if(view == null) + continue; + activity.runOnUiThread(new Runnable() { + public void run() { + decoration.decoration.reapply(activity, view); + } + }); + } + } + } + }; - // context menu listener - container.setOnCreateContextMenuListener(listener); + /** + * Respond to a request to add details for a task + * + * @param taskId + */ + public synchronized void addDecorations(ListView listView, long taskId, + TaskDecoration taskDecoration) { + if(taskDecoration == null) + return; - // tap listener - container.setOnClickListener(listener); + ArrayList cacheList = decorationCache.addIfNotExists(taskId, + taskDecoration); + if(cacheList != null) { + // update view if it is visible + int length = listView.getChildCount(); + for(int i = 0; i < length; i++) { + ViewHolder viewHolder = (ViewHolder) listView.getChildAt(i).getTag(); + if(viewHolder == null || viewHolder.task.getId() != taskId) + continue; + decorate(viewHolder, cacheList); + break; + } + } + } + + private void decorate(ViewHolder viewHolder, ArrayList decorations) { + if(decorations == null || decorations.size() == 0) + return; + + // apply decorations in reverse so top priority appears at the ends & + // color is set bu the most important decoration + viewHolder.decorations = new View[decorations.size()]; + for(int i = decorations.size() - 1; i >= 0; i--) { + TaskDecoration deco = decorations.get(i); + if(deco.color != 0) + ((View)viewHolder.taskRow.getParent()).setBackgroundColor(deco.color); + if(deco.decoration != null) { + View view = deco.decoration.apply(activity, viewHolder.taskRow); + viewHolder.decorations[i] = view; + switch(deco.position) { + case TaskDecoration.POSITION_LEFT: + viewHolder.taskRow.addView(view, 1); + break; + case TaskDecoration.POSITION_RIGHT: + viewHolder.taskRow.addView(view, viewHolder.taskRow.getChildCount() - 2); + } + } + } } /* ====================================================================== * ======================================================= event handlers * ====================================================================== */ + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + fontSize = Preferences.getIntegerFromString(R.string.p_fontSize); + } - class ContextMenuListener implements OnCreateContextMenuListener, OnClickListener { + class TaskRowListener implements OnCreateContextMenuListener, OnClickListener { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { @@ -428,11 +554,23 @@ public class TaskAdapter extends CursorAdapter { @Override public void onClick(View v) { + // expand view final ViewHolder viewHolder = (ViewHolder)v.getTag(); viewHolder.expanded = !viewHolder.expanded; LinearLayout actions = viewHolder.actions; + TextView extendedDetails = viewHolder.extendedDetails; + actions.setVisibility(viewHolder.expanded ? View.VISIBLE : View.GONE); - if(viewHolder.expanded && actions.getChildCount() == 0) { + if(!viewHolder.expanded) { + viewHolder.extendedDetails.setVisibility(View.GONE); + return; + } + + final long taskId = viewHolder.task.getId(); + extendedDetails.setText(""); //$NON-NLS-1$ + retrieveDetails(viewHolder, true); + + if(actions.getChildCount() == 0) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT, 1f); Button edit = new Button(activity); @@ -442,7 +580,7 @@ public class TaskAdapter extends CursorAdapter { @Override public void onClick(View view) { Intent intent = new Intent(activity, TaskEditActivity.class); - intent.putExtra(TaskEditActivity.ID_TOKEN, viewHolder.task.getId()); + intent.putExtra(TaskEditActivity.ID_TOKEN, taskId); activity.startActivityForResult(intent, TaskListActivity.ACTIVITY_EDIT_TASK); } }); @@ -507,4 +645,53 @@ public class TaskAdapter extends CursorAdapter { } } + /* ====================================================================== + * =============================================================== caches + * ====================================================================== */ + + private static class TaskCache { + + private final Map> cache = + Collections.synchronizedMap(new SoftHashMap>()); + + /** + * Retrieves a list. If it doesn't exist, list is created, but + * the method will return null + * @param taskId + * @return list if there was already one + */ + public ArrayList initialize(long taskId) { + if(cache.containsKey(taskId)) + return cache.get(taskId); + cache.put(taskId, new ArrayList()); + return null; + } + + /** + * Adds an item to the cache if it doesn't exist + * @param taskId + * @param item + * @return list if item was added, null if it already existed + */ + public ArrayList addIfNotExists(long taskId, TYPE item) { + ArrayList list = cache.get(taskId); + if(list == null || list.contains(item)) + return null; + list.add(item); + return list; + } + + /** + * Clears the cache + */ + public void clear() { + cache.clear(); + } + + public ArrayList get(long taskId) { + return cache.get(taskId); + } + + } + } diff --git a/astrid/src/com/todoroo/astrid/api/DetailExposer.java b/astrid/src/com/todoroo/astrid/api/DetailExposer.java index 2a4e84e60..804b7c846 100644 --- a/astrid/src/com/todoroo/astrid/api/DetailExposer.java +++ b/astrid/src/com/todoroo/astrid/api/DetailExposer.java @@ -12,8 +12,13 @@ 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 TaskDetail getTaskDetails(Context context, long id); + public String getTaskDetails(Context context, long id, boolean extended); }