Notifications seem to work pretty well. Needs testing.

pull/14/head
Tim Su 16 years ago
parent 4d46900f45
commit 2f4c104d83

@ -5,7 +5,6 @@
android:versionName="1.7">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
@ -23,6 +22,9 @@
<activity android:name=".activities.TagList"/>
<receiver android:name=".utilities.Notifications">
</receiver>
<receiver android:name=".utilities.StartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />

@ -147,6 +147,7 @@
<!-- Dialog Boxes -->
<skip />
<string name="information_title">Information</string>
<string name="question_title">Question</string>
<string name="yes">Yes</string>
<string name="no">No</string>
@ -159,5 +160,10 @@
<string name="alarm_toast_title">Next notification time: </string>
<!-- Dialog Boxes -->
<skip />
<string name="error_opening">Couldn't find this item:</string>
<string name="error_saving">Couldn't save:</string>
</resources>

@ -18,7 +18,11 @@
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.timsu.astrid.activities;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
@ -26,6 +30,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
@ -46,7 +51,9 @@ import com.timsu.astrid.R;
import com.timsu.astrid.data.tag.TagController;
import com.timsu.astrid.data.tag.TagIdentifier;
import com.timsu.astrid.data.tag.TagModelForView;
import com.timsu.astrid.data.task.TaskController;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.data.task.TaskModelForList;
/** List all tags and allows a user to see all tasks for a given tag
@ -64,26 +71,91 @@ public class TagList extends Activity {
private static final int CONTEXT_DELETE_ID = Menu.FIRST + 11;
private TagController controller;
private TaskController taskController;
private ListView listView;
private List<TagModelForView> tagArray;
private Map<Long, TaskModelForList> taskMap;
private Map<TagModelForView, Integer> tagToTaskCount;
private SortMode sortMode = SortMode.SIZE;
private boolean sortReverse = false;
/** Called when loading up the activity for the first time */
private void onLoad() {
controller = new TagController(this);
controller.open();
taskController = new TaskController(this);
taskController.open();
listView = (ListView)findViewById(R.id.taglist);
fillData();
}
// --- stuff for sorting
private enum SortMode {
ALPHA {
@Override
int compareTo(TagList self, TagModelForView arg0, TagModelForView arg1) {
return arg0.getName().compareTo(arg1.getName());
}
},
SIZE {
@Override
int compareTo(TagList self, TagModelForView arg0, TagModelForView arg1) {
return self.tagToTaskCount.get(arg0) - self.tagToTaskCount.get(arg1);
}
};
abstract int compareTo(TagList self, TagModelForView arg0, TagModelForView arg1);
};
private void sortTagArray() {
// get all tasks
Cursor taskCursor = taskController.getActiveTaskListCursor();
startManagingCursor(taskCursor);
List<TaskModelForList> taskArray =
taskController.createTaskListFromCursor(taskCursor, true);
taskMap = new HashMap<Long, TaskModelForList>();
for(TaskModelForList task : taskArray)
taskMap.put(task.getTaskIdentifier().getId(), task);
// get accurate task count for each tag
tagToTaskCount = new HashMap<TagModelForView, Integer>();
for(TagModelForView tag : tagArray) {
int count = 0;
List<TaskIdentifier> tasks = controller.getTaggedTasks(TagList.this,
tag.getTagIdentifier());
for(TaskIdentifier taskId : tasks)
if(taskMap.containsKey(taskId.getId()))
count++;
tagToTaskCount.put(tag, count);
}
// do sort
Collections.sort(tagArray, new Comparator<TagModelForView>() {
@Override
public int compare(TagModelForView arg0, TagModelForView arg1) {
return sortMode.compareTo(TagList.this, arg0, arg1);
}
});
if(sortReverse)
Collections.reverse(tagArray);
}
// --- fill data
/** Fill in the Tag List with our tags */
private void fillData() {
Resources r = getResources();
tagArray = controller.getAllTags(this);
// perform sort
sortTagArray();
// set up the title
StringBuilder title = new StringBuilder().
append(r.getString(R.string.tagList_titlePrefix)).
@ -165,12 +237,9 @@ public class TagList extends Activity {
// set up basic properties
view.setTag(tag);
List<TaskIdentifier> tasks = controller.getTaggedTasks(TagList.this,
tag.getTagIdentifier());
final TextView name = ((TextView)view.findViewById(android.R.id.text1));
name.setText(new StringBuilder(tag.getName()).
append(" (").append(tasks.size()).append(")"));
append(" (").append(tagToTaskCount.get(tag)).append(")"));
}
}
@ -202,6 +271,22 @@ public class TagList extends Activity {
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch(item.getItemId()) {
case MENU_SORT_ALPHA_ID:
if(sortMode == SortMode.ALPHA)
sortReverse = !sortReverse;
else {
sortMode = SortMode.ALPHA;
sortReverse = false;
}
break;
case MENU_SORT_SIZE_ID:
if(sortMode == SortMode.SIZE)
sortReverse = !sortReverse;
else {
sortMode = SortMode.SIZE;
sortReverse = false;
}
break;
case CONTEXT_CREATE_ID:
TagModelForView tag = tagArray.get(item.getGroupId());
createTask(tag);
@ -229,6 +314,7 @@ public class TagList extends Activity {
protected void onDestroy() {
super.onDestroy();
controller.close();
taskController.close();
}
@Override

@ -195,11 +195,11 @@ public class TaskEdit extends TaskModificationActivity<TaskModelForEdit> {
controller.saveTask(model);
saveTags();
} catch (Exception e) {
Log.e(getClass().getSimpleName(), "Error saving task!", e); // TODO
showErrorAndFinish(R.string.error_saving, e);
}
// set up notification
Notifications.scheduleNextAlarm(this, model);
Notifications.updateAlarm(this, model);
}
/** Save task tags. Must be called after task already has an ID */
@ -262,7 +262,7 @@ public class TaskEdit extends TaskModificationActivity<TaskModelForEdit> {
TimeDurationType.HOURS_MINUTES);
notification = new TimeDurationControlSet(this, R.id.notification,
R.string.notification_prefix, R.string.notification_dialog,
TimeDurationType.HOURS_MINUTES);
TimeDurationType.DAYS_HOURS);
definiteDueDate = new DateControlSet(this, R.id.definiteDueDate_notnull,
R.id.definiteDueDate_date, R.id.definiteDueDate_time);
preferredDueDate = new DateControlSet(this, R.id.preferredDueDate_notnull,

@ -20,11 +20,15 @@
package com.timsu.astrid.activities;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import com.timsu.astrid.R;
import com.timsu.astrid.data.task.AbstractTaskModel;
import com.timsu.astrid.data.task.TaskController;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.utilities.DialogUtilities;
/** Abstract activity that operates on a single task. Use the generic parameter
* to pass in the model class you are working with.
@ -43,21 +47,39 @@ public abstract class TaskModificationActivity<MODEL_TYPE extends
controller = new TaskController(this);
controller.open();
// check if we have a TaskIdentifier
TaskIdentifier identifier = null;
Bundle extras = getIntent().getExtras();
if(savedInstanceState != null && savedInstanceState.containsKey(LOAD_INSTANCE_TOKEN)) {
identifier = new TaskIdentifier(savedInstanceState.getLong(
LOAD_INSTANCE_TOKEN));
} else if(extras != null && extras.containsKey(LOAD_INSTANCE_TOKEN))
identifier = new TaskIdentifier(extras.getLong(
LOAD_INSTANCE_TOKEN));
try {
// check if we have a TaskIdentifier
TaskIdentifier identifier = null;
Bundle extras = getIntent().getExtras();
if(savedInstanceState != null && savedInstanceState.containsKey(LOAD_INSTANCE_TOKEN)) {
identifier = new TaskIdentifier(savedInstanceState.getLong(
LOAD_INSTANCE_TOKEN));
} else if(extras != null && extras.containsKey(LOAD_INSTANCE_TOKEN))
identifier = new TaskIdentifier(extras.getLong(
LOAD_INSTANCE_TOKEN));
model = getModel(identifier);
model = getModel(identifier);
} catch (Exception e) {
showErrorAndFinish(R.string.error_opening, e);
}
}
protected void showErrorAndFinish(int prefix, Throwable e) {
Resources r = getResources();
DialogUtilities.okDialog(this,
r.getString(prefix) + " " +
e.getLocalizedMessage(), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
}
abstract protected MODEL_TYPE getModel(TaskIdentifier identifier);
@Override
protected void onDestroy() {
super.onDestroy();

@ -108,7 +108,7 @@ public class TaskView extends TaskModificationActivity<TaskModelForView> {
// clear residual, schedule the next one
Notifications.clearAllNotifications(this, model.getTaskIdentifier());
Notifications.scheduleNextAlarm(this, model);
Notifications.updateAlarm(this, model);
String[] responses = r.getStringArray(R.array.reminder_responses);
String response = responses[new Random().nextInt(responses.length)];

@ -41,6 +41,7 @@ public class TaskModelForEdit extends AbstractTaskModel implements Notifiable {
HIDDEN_UNTIL,
BLOCKING_ON,
NOTIFICATIONS,
PROGRESS_PERCENTAGE,
NOTES,
};
@ -57,6 +58,11 @@ public class TaskModelForEdit extends AbstractTaskModel implements Notifiable {
// --- getters and setters
@Override
public boolean isTaskCompleted() {
return super.isTaskCompleted();
}
@Override
public Integer getNotificationIntervalSeconds() {
return super.getNotificationIntervalSeconds();

@ -35,6 +35,7 @@ public class TaskModelForNotify extends AbstractTaskModel implements Notifiable
AbstractController.KEY_ROWID,
NOTIFICATIONS,
HIDDEN_UNTIL,
PROGRESS_PERCENTAGE,
};
// --- constructors
@ -43,10 +44,16 @@ public class TaskModelForNotify extends AbstractTaskModel implements Notifiable
super(cursor);
getNotificationIntervalSeconds();
getHiddenUntil();
isTaskCompleted();
}
// --- getters and setters
@Override
public boolean isTaskCompleted() {
return super.isTaskCompleted();
}
@Override
public Integer getNotificationIntervalSeconds() {
return super.getNotificationIntervalSeconds();

@ -0,0 +1,20 @@
package com.timsu.astrid.utilities;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import com.timsu.astrid.R;
public class DialogUtilities {
public static void okDialog(Context context, String text,
DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(context)
.setTitle(R.string.information_title)
.setMessage(text)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, okListener)
.show();
}
}

@ -23,19 +23,24 @@ import com.timsu.astrid.data.task.TaskModelForNotify;
public class Notifications extends BroadcastReceiver {
private static final String ID_KEY = "id";
private static final int MIN_INTERVAL_SECONDS = 120;
private static Random random = new Random();
private static final String ID_KEY = "id";
private static final int MIN_INTERVAL_SECONDS = 300;
private static final float FUDGE_MIN = 0.2f;
private static final float FUDGE_MAX = 0.8f;
private static Random random = new Random();
/** Something we can create a notification for */
public interface Notifiable {
public TaskIdentifier getTaskIdentifier();
public Integer getNotificationIntervalSeconds();
public boolean isTaskCompleted();
public Date getHiddenUntil();
}
@Override
/** Startup intent */
/** Alarm intent */
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(ID_KEY, 0);
Log.e("ALARM", "Alarm triggered id " + id);
@ -44,58 +49,73 @@ public class Notifications extends BroadcastReceiver {
// --- alarm manager stuff
private static boolean isAlarmEnabled(Notifiable task) {
if(task.getNotificationIntervalSeconds() == null ||
task.getNotificationIntervalSeconds() == 0)
return false;
if(task.getHiddenUntil() != null && task.getHiddenUntil().after(new Date()))
return false;
if(task.isTaskCompleted())
return false;
return true;
}
public static void scheduleAllAlarms(Context context) {
TaskController controller = new TaskController(context);
controller.open();
List<TaskModelForNotify> tasks = controller.getTasksWithNotifications();
for(TaskModelForNotify task : tasks)
scheduleNextAlarm(context, task);
updateAlarm(context, task);
}
/** Schedules the next notification for this task */
public static void scheduleNextAlarm(Context context,
Notifiable task) {
if(task.getNotificationIntervalSeconds() == null ||
task.getNotificationIntervalSeconds() == 0 ||
task.getTaskIdentifier() == null)
public static void updateAlarm(Context context, Notifiable task) {
if(task.getTaskIdentifier() == null)
return;
if(task.getHiddenUntil() != null && task.getHiddenUntil().after(new Date()))
if(!isAlarmEnabled(task)) {
deleteAlarm(context, task.getTaskIdentifier().getId());
return;
}
// compute, and add a fudge factor to mix things up a bit
int interval = task.getNotificationIntervalSeconds();
int currentSeconds = (int)(System.currentTimeMillis() / 1000);
int untilNextInterval = interval - currentSeconds % interval;
untilNextInterval *= 0.2f + random.nextFloat() * 0.6f;
untilNextInterval *= FUDGE_MIN + random.nextFloat() * (FUDGE_MAX - FUDGE_MIN);
if(untilNextInterval < MIN_INTERVAL_SECONDS)
untilNextInterval = MIN_INTERVAL_SECONDS;
long when = System.currentTimeMillis() + untilNextInterval * 1000;
scheduleAlarm(context, task.getTaskIdentifier().getId(), when);
scheduleAlarm(context, task.getTaskIdentifier().getId(), when,
interval*1000);
}
/** Delete the given alarm */
public static void deleteAlarm(Context context, long id) {
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
private static PendingIntent createPendingIntent(Context context, long id) {
Intent intent = new Intent(context, Notifications.class);
intent.setType(Long.toString(id));
intent.putExtra(ID_KEY, id);
PendingIntent sender = PendingIntent.getBroadcast(context, 0, intent, 0);
am.cancel(sender);
return sender;
}
/** Schedules a single alarm */
public static void scheduleAlarm(Context context, long id, long when) {
/** Delete the given alarm */
public static void deleteAlarm(Context context, long id) {
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, Notifications.class);
intent.putExtra(ID_KEY, id);
PendingIntent sender = PendingIntent.getBroadcast(context, 0, intent, 0);
am.cancel(createPendingIntent(context, id));
}
/** Schedules a recurring alarm for a single task */
public static void scheduleAlarm(Context context, long id, long when, long interval) {
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Log.e("ALARM", "Alarm set for " + new Date(when));
am.set(AlarmManager.RTC, when, sender);
am.setRepeating(AlarmManager.RTC, when, interval, createPendingIntent(context, id));
}
// --- notification manager stuff
@ -136,8 +156,6 @@ public class Notifications extends BroadcastReceiver {
pendingIntent);
notification.defaults = Notification.DEFAULT_ALL;
notification.vibrate = new long[] { 300, 50, 50, 300, 100, 300, 100,
100, 200 };
Log.w("Notifications", "Logging notification: " + reminder);
nm.notify((int)id, notification);

@ -8,7 +8,7 @@ public class StartupReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// do nothing...?
Notifications.scheduleAllAlarms(context);
}
}

Loading…
Cancel
Save