First pass at task decorations, timer. Works but doesn't update.

pull/14/head
Tim Su 14 years ago
parent 95154ca51c
commit 8f757ac9fa

@ -207,6 +207,14 @@
</receiver>
<!-- notes -->
<!-- timers -->
<receiver android:name="com.todoroo.astrid.timers.TimerDecorationExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DECORATIONS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- reminders -->
<activity android:name="com.todoroo.astrid.reminders.ReminderPreferences"

@ -47,6 +47,11 @@ public class AstridApiConstants {
*/
public static final String EXTRAS_ADDON = "addon";
/**
* Extras name for whether task detail request is extended
*/
public static final String EXTRAS_EXTENDED = "extended";
// --- Add-ons API
/**
@ -89,11 +94,14 @@ public class AstridApiConstants {
*/
public static final String BROADCAST_SEND_EDIT_CONTROLS = PACKAGE + ".SEND_EDIT_CONTROLS";
// --- Task List Details API
// --- Task Details API
/**
* Action name for broadcast intent requesting task list details for a task
* Action name for broadcast intent requesting details for a task.
* Extended details are displayed when a user presses on a task.
*
* @extra EXTRAS_TASK_ID id of the task
* @extra EXTRAS_EXTENDED whether request is for standard or extended details
*/
public static final String BROADCAST_REQUEST_DETAILS = PACKAGE + ".REQUEST_DETAILS";
@ -101,10 +109,43 @@ public class AstridApiConstants {
* Action name for broadcast intent sending details back to Astrid
* @extra EXTRAS_ADDON your add-on identifier
* @extra EXTRAS_TASK_ID id of the task
* @extra EXTRAS_RESPONSE a {@link TaskDetail} object
* @extra EXTRAS_EXTENDED whether request is for standard or extended details
* @extra EXTRAS_RESPONSE a String
*/
public static final String BROADCAST_SEND_DETAILS = PACKAGE + ".SEND_DETAILS";
// --- Task Actions API
/**
* Action name for broadcast intent requesting actions for a task
* @extra EXTRAS_TASK_ID id of the task
*/
public static final String BROADCAST_REQUEST_ACTIONS = PACKAGE + ".REQUEST_ACTIONS";
/**
* Action name for broadcast intent sending actions back to Astrid
* @extra EXTRAS_ADDON your add-on identifier
* @extra EXTRAS_TASK_ID id of the task
* @extra EXTRAS_RESPONSE a String
*/
public static final String BROADCAST_SEND_ACTIONS = PACKAGE + ".SEND_ACTIONS";
// --- Task Decorations API
/**
* Action name for broadcast intent requesting task list decorations for a task
* @extra EXTRAS_TASK_ID id of the task
*/
public static final String BROADCAST_REQUEST_DECORATIONS = PACKAGE + ".REQUEST_DECORATIONS";
/**
* Action name for broadcast intent sending decorations back to Astrid
* @extra EXTRAS_ADDON your add-on identifier
* @extra EXTRAS_TASK_ID id of the task
* @extra EXTRAS_RESPONSE a {@link TaskDecoration}
*/
public static final String BROADCAST_SEND_DECORATIONS = PACKAGE + ".SEND_DECORATIONS";
// --- Actions API
/**

@ -8,17 +8,12 @@ import android.os.Parcel;
import android.os.Parcelable;
/**
* Represents an intent that can be called on a task being edited
* Represents an intent that can be called on a task
*
* @author Tim Su <tim@todoroo.com>
*
*/
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<EditOperation> CREATOR = new Parcelable.Creator<EditOperation>() {
public static final Parcelable.Creator<TaskAction> CREATOR = new Parcelable.Creator<TaskAction>() {
/**
* {@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];
};
};

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

@ -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 <tim@todoroo.com>
*
*/
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 <code>0</code> 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<TaskDecoration> CREATOR = new Parcelable.Creator<TaskDecoration>() {
/**
* {@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];
};
};
}

@ -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 <tim@todoroo.com>
*
*/
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 <code>0</code> 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<TaskDetail> CREATOR = new Parcelable.Creator<TaskDetail>() {
/**
* {@inheritDoc}
*/
public TaskDetail createFromParcel(Parcel source) {
return new TaskDetail(source.readString(), source.readInt());
}
/**
* {@inheritDoc}
*/
public TaskDetail[] newArray(int size) {
return new TaskDetail[size];
};
};
}

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

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

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

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

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

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

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

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

@ -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 <tim@todoroo.com>
*
*/
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<Long, TaskDecoration> decorations =
new HashMap<Long, TaskDecoration>();
private static HashMap<Long, Timer> timers =
new HashMap<Long, Timer>();
@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);
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

@ -10,7 +10,7 @@
android:paddingRight="6dip"
android:orientation="vertical">
<LinearLayout
<LinearLayout android:id="@+id/task_row"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="100"
@ -61,6 +61,11 @@
android:layout_height="fill_parent" />
</LinearLayout>
<TextView android:id="@+id/extendedDetails"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<LinearLayout android:id="@+id/actions"
android:layout_width="fill_parent"

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- See the file "LICENSE" for the full license governing this code. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent">
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scaleType="center"
android:src="@drawable/timers_decoration" />
<TextView android:id="@+id/timer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="10sp" />
</LinearLayout>

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

@ -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<Long, Boolean> completedItems;
public boolean isFling = false;
private final int resource;
private final LayoutInflater inflater;
protected OnCompletedTaskListener onCompletedTaskListener = null;
private int fontSize;
private static final SoftHashMap<Long, LinkedHashSet<TaskDetail>> detailCache =
new SoftHashMap<Long, LinkedHashSet<TaskDetail>>();
// --- task detail and decoration soft caches
private static final TaskCache<String> detailCache = new TaskCache<String>();
private static final TaskCache<String> extendedDetailCache = new TaskCache<String>();
private static final TaskCache<TaskDecoration> decorationCache = new TaskCache<TaskDecoration>();
/**
* 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<Task> 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<TaskDetail> details;
synchronized(detailCache) {
if(detailCache.containsKey(taskId))
inCache = true;
else
detailCache.put(taskId, new LinkedHashSet<TaskDetail>());
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<String> cache = extended ? extendedDetailCache : detailCache;
ArrayList<String> 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<String> 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<TaskDetail> details) {
private void spanifyAndAdd(ViewHolder viewHolder, boolean extended, ArrayList<String> 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<TaskDetail> iterator = details.iterator(); iterator.hasNext(); ) {
detailText.append(iterator.next().text);
for(Iterator<String> 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<TaskDetail> 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<String> cache = extended ? extendedDetailCache : detailCache;
ArrayList<String> 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<TaskDecoration> 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<TaskDecoration> 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<TaskDecoration> 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<TaskDecoration> 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<TYPE> {
private final Map<Long, ArrayList<TYPE>> cache =
Collections.synchronizedMap(new SoftHashMap<Long, ArrayList<TYPE>>());
/**
* 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<TYPE> initialize(long taskId) {
if(cache.containsKey(taskId))
return cache.get(taskId);
cache.put(taskId, new ArrayList<TYPE>());
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<TYPE> addIfNotExists(long taskId, TYPE item) {
ArrayList<TYPE> 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<TYPE> get(long taskId) {
return cache.get(taskId);
}
}
}

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

Loading…
Cancel
Save