diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3fbe3773f..a31f6101c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4,9 +4,7 @@ android:versionCode="101" android:versionName="2.7.3"> - + @@ -18,9 +16,7 @@ - + @@ -70,12 +66,17 @@ - + + + + + + + + @@ -83,9 +84,7 @@ - + diff --git a/res/drawable/bubble_blue.png b/res/drawable/bubble_blue.png deleted file mode 100644 index e82e1acfb..000000000 Binary files a/res/drawable/bubble_blue.png and /dev/null differ diff --git a/res/drawable/bubble_gray.png b/res/drawable/bubble_gray.png deleted file mode 100644 index ac071326a..000000000 Binary files a/res/drawable/bubble_gray.png and /dev/null differ diff --git a/res/drawable/bubble_red.png b/res/drawable/bubble_red.png deleted file mode 100644 index e639565ad..000000000 Binary files a/res/drawable/bubble_red.png and /dev/null differ diff --git a/res/drawable/bubble_yellow.png b/res/drawable/bubble_yellow.png deleted file mode 100644 index c4a2ee2c3..000000000 Binary files a/res/drawable/bubble_yellow.png and /dev/null differ diff --git a/res/drawable/notif_tag.png b/res/drawable/notif_tag.png new file mode 100644 index 000000000..a519a5e58 Binary files /dev/null and b/res/drawable/notif_tag.png differ diff --git a/src/com/timsu/astrid/activities/LocaleEditAlerts.java b/src/com/timsu/astrid/activities/LocaleEditAlerts.java index 40659b8fa..d1b3af1e4 100644 --- a/src/com/timsu/astrid/activities/LocaleEditAlerts.java +++ b/src/com/timsu/astrid/activities/LocaleEditAlerts.java @@ -28,11 +28,13 @@ import com.timsu.astrid.data.tag.TagModelForView; public final class LocaleEditAlerts extends Activity { /** value for action type for tag alert */ - public static final String ACTION_TAG_ALERT = "com.timsu.astrid.action.TAG_ALERT"; + public static final String ACTION_LOCALE_ALERT = "com.timsu.astrid.action.LOCALE_ALERT"; - /** key name for tag name in bundle */ - public static final String KEY_TAG_NAME = "tag"; + /** key name for tag id/name in bundle */ + public static final String KEY_TAG_ID = "tag"; + public static final String KEY_TAG_NAME = "name"; + private LinkedList tags = null; private String[] tagNames = null; /** Called when the activity is first created. */ @@ -59,7 +61,7 @@ public final class LocaleEditAlerts extends Activity { TagController tagController = new TagController(this); tagController.open(); - LinkedList tags = tagController.getAllTags(); + tags = tagController.getAllTags(); tagController.close(); tagNames = new String[tags.size()]; @@ -91,7 +93,9 @@ public final class LocaleEditAlerts extends Activity { * Private helper method to persist the Toast message in the return {@code Intent}. */ private void updateResult() { - final String tagName = (String)((Spinner) findViewById(R.id.spinner)).getSelectedItem(); + final int index = ((Spinner) findViewById(R.id.spinner)).getSelectedItemPosition(); + final String tagName = tagNames[index]; + final TagModelForView tag = tags.get(index); /* * If the message is of 0 length, then there isn't a setting to save @@ -101,7 +105,8 @@ public final class LocaleEditAlerts extends Activity { } else { final Intent intent = new Intent(); intent.putExtra(com.twofortyfouram.Intent.EXTRA_STRING_ACTION_FIRE, - ACTION_TAG_ALERT); + ACTION_LOCALE_ALERT); + intent.putExtra(KEY_TAG_ID, tag.getTagIdentifier().getId()); intent.putExtra(KEY_TAG_NAME, tagName); intent.putExtra(com.twofortyfouram.Intent.EXTRA_STRING_BLURB, tagName); setResult(RESULT_OK, intent); diff --git a/src/com/timsu/astrid/activities/TagListSubActivity.java b/src/com/timsu/astrid/activities/TagListSubActivity.java index 41358830c..8457c2d99 100644 --- a/src/com/timsu/astrid/activities/TagListSubActivity.java +++ b/src/com/timsu/astrid/activities/TagListSubActivity.java @@ -126,33 +126,32 @@ public class TagListSubActivity extends SubActivity { abstract int compareTo(TagListSubActivity self, TagModelForView arg0, TagModelForView arg1); }; + /** Counts how many tasks appear in active task list */ + public static int countActiveTasks(HashSet activeTasks, LinkedList tasks) { + int count = 0; + if(tasks != null) { + for(TaskIdentifier task : tasks) + if(activeTasks.contains(task)) + count++; + } + return count; + } + private synchronized void sortTagArray() { // get all tasks HashSet activeTasks = getTaskController().getActiveTaskIdentifiers(); + if(activeTasks == null) + activeTasks = new HashSet(); // get task count for each tag tagToTaskCount = new HashMap(); + for(TagModelForView tag : tagArray) { LinkedList tasks; - - // Tagged vs. Untagged tasks - boolean taggedTask = !tag.getTagIdentifier().equals(TagModelForView.UNTAGGED_IDENTIFIER); - if (taggedTask) - tasks = getTagController().getTaggedTasks(tag.getTagIdentifier()); - else - tasks = getTagController().getUntaggedTasks(activeTasks); - - int count = 0; - for(TaskIdentifier task : tasks) - if(activeTasks.contains(task)) - count++; - - // don't show Untagged if there aren't any untagged tasks - if (taggedTask || count!=0) - tagToTaskCount.put(tag, count); - else - tagArray.remove(tag); + tasks = getTagController().getTaggedTasks(tag.getTagIdentifier()); + int count = countActiveTasks(activeTasks, tasks); + tagToTaskCount.put(tag, count); } // do sort @@ -161,6 +160,14 @@ public class TagListSubActivity extends SubActivity { return sortMode.compareTo(TagListSubActivity.this, arg0, arg1); } }); + + // show "untagged" as a category at the top, in the proper language/localization + String untaggedLabel = getResources().getString(R.string.tagList_untagged); + TagModelForView untaggedModel = TagModelForView.getUntaggedModel(untaggedLabel); + tagArray.addFirst(untaggedModel); + int count = countActiveTasks(activeTasks, getTagController().getUntaggedTasks()); + tagToTaskCount.put(untaggedModel, count); + if(sortReverse) Collections.reverse(tagArray); } @@ -172,9 +179,6 @@ public class TagListSubActivity extends SubActivity { try { tagArray = getTagController().getAllTags(); - // Show "Untagged" as a category, in the proper language/localization - String untaggedLabel = getResources().getString(R.string.tagList_untagged); - tagArray.add(TagModelForView.getUntaggedModel(untaggedLabel)); sortTagArray(); // count and sort each tag } catch (StaleDataException e) { // happens when you rotate the screen while the thread is diff --git a/src/com/timsu/astrid/activities/TaskListSubActivity.java b/src/com/timsu/astrid/activities/TaskListSubActivity.java index 6c2c66a55..4901e441e 100644 --- a/src/com/timsu/astrid/activities/TaskListSubActivity.java +++ b/src/com/timsu/astrid/activities/TaskListSubActivity.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; @@ -494,9 +493,7 @@ public class TaskListSubActivity extends SubActivity { if (!tagId.equals(TagModelForView.UNTAGGED_IDENTIFIER)) { tasks = getTagController().getTaggedTasks(tagId); } else { - HashSet activeTasks = - getTaskController().getActiveTaskIdentifiers(); - tasks = getTagController().getUntaggedTasks(activeTasks); + tasks = getTagController().getUntaggedTasks(); } tasksCursor = getTaskController().getTaskListCursorById(tasks); diff --git a/src/com/timsu/astrid/data/enums/Importance.java b/src/com/timsu/astrid/data/enums/Importance.java index ddfbdd162..b7082d225 100644 --- a/src/com/timsu/astrid/data/enums/Importance.java +++ b/src/com/timsu/astrid/data/enums/Importance.java @@ -26,20 +26,16 @@ public enum Importance { LEVEL_1(R.string.importance_1, R.color.importance_1, - R.color.task_list_importance_1, - R.drawable.bubble_red), + R.color.task_list_importance_1), LEVEL_2(R.string.importance_2, R.color.importance_2, - R.color.task_list_importance_2, - R.drawable.bubble_yellow), + R.color.task_list_importance_2), LEVEL_3(R.string.importance_3, R.color.importance_3, - R.color.task_list_importance_3, - R.drawable.bubble_blue), + R.color.task_list_importance_3), LEVEL_4(R.string.importance_4, R.color.importance_4, - R.color.task_list_importance_4, - R.drawable.bubble_gray), + R.color.task_list_importance_4), // LEAST IMPORTANT ; @@ -47,14 +43,12 @@ public enum Importance { int label; int color; int taskListColor; - int icon; public static final Importance DEFAULT = LEVEL_3; - private Importance(int label, int color, int taskListColor, int icon) { + private Importance(int label, int color, int taskListColor) { this.label = label; this.color = color; this.taskListColor = taskListColor; - this.icon = icon; } public int getLabelResource() { @@ -69,7 +63,4 @@ public enum Importance { return taskListColor; } - public int getIconResource() { - return icon; - } } diff --git a/src/com/timsu/astrid/data/tag/TagController.java b/src/com/timsu/astrid/data/tag/TagController.java index 31856a8e8..facebba99 100644 --- a/src/com/timsu/astrid/data/tag/TagController.java +++ b/src/com/timsu/astrid/data/tag/TagController.java @@ -20,12 +20,12 @@ package com.timsu.astrid.data.tag; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; -import android.app.Activity; import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; +import android.database.CursorJoiner; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; @@ -33,6 +33,7 @@ import com.timsu.astrid.data.AbstractController; import com.timsu.astrid.data.tag.AbstractTagModel.TagModelDatabaseHelper; import com.timsu.astrid.data.tag.TagToTaskMapping.TagToTaskMappingDatabaseHelper; import com.timsu.astrid.data.task.TaskIdentifier; +import com.timsu.astrid.data.task.AbstractTaskModel.TaskModelDatabaseHelper; /** Controller for Tag-related operations */ public class TagController extends AbstractController { @@ -118,7 +119,6 @@ public class TagController extends AbstractController { return list; } - /** Returns a list of task identifiers in the provided set that are UNtagged. * * The calling SubActivity must provide the set of tasks, since @@ -127,26 +127,43 @@ public class TagController extends AbstractController { * The current implementation is not very efficient, because queries * the TagToTask map once for each active task. **/ - public LinkedList getUntaggedTasks(HashSet - activeTasks) throws SQLException { - + public LinkedList getUntaggedTasks() throws SQLException { LinkedList list = new LinkedList(); - for (TaskIdentifier taskId : activeTasks) { - Cursor cursor; - cursor = tagToTaskMapDatabase.query(TAG_TASK_MAP_NAME, - TagToTaskMapping.FIELD_LIST, TagToTaskMapping.TASK + " = ?", - new String[] { taskId.idAsString() }, null, null, null); + String[] tagMapColumns = new String[] { TagToTaskMapping.TASK }; + Cursor tagMapCursor = tagToTaskMapDatabase.query(TAG_TASK_MAP_NAME, + tagMapColumns, null, null, TagToTaskMapping.TASK, null, + TagToTaskMapping.TASK + " ASC"); - if (cursor.getCount() == 0) { - list.add(taskId); - } - cursor.close(); - } + SQLiteDatabase taskDatabase = new TaskModelDatabaseHelper(context, + TASK_TABLE_NAME, TASK_TABLE_NAME).getReadableDatabase(); + String[] taskColumns = new String[] { KEY_ROWID }; + Cursor taskCursor = taskDatabase.query(TASK_TABLE_NAME, taskColumns, + null, null, null, null, KEY_ROWID + " ASC"); - // Equivalent SQL Query: - // SELECT * FROM tasks_tbl LEFT OUTER JOIN mapping_tbl ON tasks_tbl.id = - // mapping_tbl.task_id WHERE mapping_tbl.task_id ISNULL; + try { + CursorJoiner joiner = new CursorJoiner(taskCursor, taskColumns, + tagMapCursor, tagMapColumns); + for (CursorJoiner.Result joinerResult : joiner) { + switch (joinerResult) { + case LEFT: + long taskId = taskCursor.getLong(0); + + // conditional is necessary for handling deleted tasks + if(tagMapCursor.isLast() || tagMapCursor.getLong(0) > taskId) { + list.add(new TaskIdentifier(taskId)); + } + break; + default: + // in other cases, do nothing + break; + } + } + } finally { + taskCursor.close(); + tagMapCursor.close(); + taskDatabase.close(); + } return list; } @@ -261,8 +278,8 @@ public class TagController extends AbstractController { * Constructor - takes the context to allow the database to be * opened/created */ - public TagController(Activity activity) { - this.context = activity; + public TagController(Context context) { + this.context = context; } /** diff --git a/src/com/timsu/astrid/data/task/AbstractTaskModel.java b/src/com/timsu/astrid/data/task/AbstractTaskModel.java index 0d796a753..b022f2a29 100644 --- a/src/com/timsu/astrid/data/task/AbstractTaskModel.java +++ b/src/com/timsu/astrid/data/task/AbstractTaskModel.java @@ -51,27 +51,28 @@ public abstract class AbstractTaskModel extends AbstractModel { // field names - static final String NAME = "name"; - static final String NOTES = "notes"; - static final String PROGRESS_PERCENTAGE = "progressPercentage"; - static final String IMPORTANCE = "importance"; - static final String ESTIMATED_SECONDS = "estimatedSeconds"; - static final String ELAPSED_SECONDS = "elapsedSeconds"; - static final String TIMER_START = "timerStart"; - static final String DEFINITE_DUE_DATE = "definiteDueDate"; - static final String PREFERRED_DUE_DATE = "preferredDueDate"; - static final String HIDDEN_UNTIL = "hiddenUntil"; - static final String POSTPONE_COUNT = "postponeCount"; - static final String NOTIFICATIONS = "notifications"; - static final String NOTIFICATION_FLAGS = "notificationFlags"; - static final String LAST_NOTIFIED = "lastNotified"; - static final String REPEAT = "repeat"; - static final String CREATION_DATE = "creationDate"; - static final String COMPLETION_DATE = "completionDate"; - static final String CALENDAR_URI = "calendarUri"; - static final String FLAGS = "flags"; + public static final String NAME = "name"; + public static final String NOTES = "notes"; + public static final String PROGRESS_PERCENTAGE = "progressPercentage"; + public static final String IMPORTANCE = "importance"; + public static final String ESTIMATED_SECONDS = "estimatedSeconds"; + public static final String ELAPSED_SECONDS = "elapsedSeconds"; + public static final String TIMER_START = "timerStart"; + public static final String DEFINITE_DUE_DATE = "definiteDueDate"; + public static final String PREFERRED_DUE_DATE = "preferredDueDate"; + public static final String HIDDEN_UNTIL = "hiddenUntil"; + public static final String POSTPONE_COUNT = "postponeCount"; + public static final String NOTIFICATIONS = "notifications"; + public static final String NOTIFICATION_FLAGS = "notificationFlags"; + public static final String LAST_NOTIFIED = "lastNotified"; + public static final String REPEAT = "repeat"; + public static final String CREATION_DATE = "creationDate"; + public static final String COMPLETION_DATE = "completionDate"; + public static final String CALENDAR_URI = "calendarUri"; + public static final String FLAGS = "flags"; + // reserved fields --- - static final String BLOCKING_ON = "blockingOn"; + public static final String BLOCKING_ON = "blockingOn"; // notification flags public static final int NOTIFY_BEFORE_DEADLINE = 1 << 0; @@ -113,10 +114,10 @@ public abstract class AbstractTaskModel extends AbstractModel { // --- database helper /** Database Helper manages creating new tables and updating old ones */ - static class TaskModelDatabaseHelper extends SQLiteOpenHelper { + public static class TaskModelDatabaseHelper extends SQLiteOpenHelper { String tableName; - TaskModelDatabaseHelper(Context context, String databaseName, String tableName) { + public TaskModelDatabaseHelper(Context context, String databaseName, String tableName) { super(context, databaseName, null, VERSION); this.tableName = tableName; } diff --git a/src/com/timsu/astrid/utilities/LocaleReceiver.java b/src/com/timsu/astrid/utilities/LocaleReceiver.java new file mode 100644 index 000000000..7652e9298 --- /dev/null +++ b/src/com/timsu/astrid/utilities/LocaleReceiver.java @@ -0,0 +1,64 @@ +package com.timsu.astrid.utilities; + +import java.util.HashSet; +import java.util.LinkedList; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.timsu.astrid.R; +import com.timsu.astrid.activities.LocaleEditAlerts; +import com.timsu.astrid.activities.TagListSubActivity; +import com.timsu.astrid.data.tag.TagController; +import com.timsu.astrid.data.tag.TagIdentifier; +import com.timsu.astrid.data.task.TaskController; +import com.timsu.astrid.data.task.TaskIdentifier; + +/** + * Receiver is activated when Locale conditions are triggered + * + * @author timsu + * + */ +public class LocaleReceiver extends BroadcastReceiver { + + @Override + /** Called when the system is started up */ + public void onReceive(Context context, Intent intent) { + try { + if (LocaleEditAlerts.ACTION_LOCALE_ALERT.equals(intent.getAction())) { + final long tagId = intent.getLongExtra(LocaleEditAlerts.KEY_TAG_ID, 0); + final String tagName = intent.getStringExtra(LocaleEditAlerts.KEY_TAG_NAME); + if(tagId == 0) { + Log.w("astrid-locale", "Invalid tag identifier in alert"); + return; + } + + // find out if we have active tasks with this tag + TaskController taskController = new TaskController(context); + taskController.open(); + TagController tagController = new TagController(context); + tagController.open(); + try { + HashSet activeTasks = taskController.getActiveTaskIdentifiers(); + LinkedList tasks = tagController.getTaggedTasks( + new TagIdentifier(tagId)); + int count = TagListSubActivity.countActiveTasks(activeTasks, tasks); + if(count > 0) { + String reminder = context.getResources().getString( + R.string.notif_tagNotification, count, tagName); + Notifications.showTagNotification(context, tagId, reminder); + } + } finally { + taskController.close(); + tagController.close(); + } + } + } catch (Exception e) { + Log.e("astrid-localerx", "Error receiving intent", e); + } + } + +} diff --git a/src/com/timsu/astrid/utilities/Notifications.java b/src/com/timsu/astrid/utilities/Notifications.java index 51b71e1ed..19bb5efa7 100644 --- a/src/com/timsu/astrid/utilities/Notifications.java +++ b/src/com/timsu/astrid/utilities/Notifications.java @@ -34,6 +34,7 @@ public class Notifications extends BroadcastReceiver { private static final String ID_KEY = "id"; private static final String FLAGS_KEY = "flags"; private static final String REPEAT_KEY = "repeat"; + private static final int TAG_ID_OFFSET = 100000; // stuff for scheduling /** minimum # of seconds before a deadline to notify */ @@ -516,5 +517,68 @@ public class Notifications extends BroadcastReceiver { return true; } + /** Schedule a new notification about the given tag. */ + public static boolean showTagNotification(Context context, long tagId, + String reminder) { + // quiet hours? only for periodic reminders + boolean quietHours = false; + Integer quietHoursStart = Preferences.getQuietHourStart(context); + Integer quietHoursEnd = Preferences.getQuietHourEnd(context); + if(quietHoursStart != null && quietHoursEnd != null) { + int hour = new Date().getHours(); + if(quietHoursStart < quietHoursEnd) { + if(hour >= quietHoursStart && hour < quietHoursEnd) + quietHours = true; + } else { // wrap across 24/hour boundary + if(hour >= quietHoursStart || hour < quietHoursEnd) + quietHours = true; + } + } + + NotificationManager nm = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + Resources r = context.getResources(); + + Intent notifyIntent = new Intent(context, TaskListNotify.class); + notifyIntent.putExtra(TaskListSubActivity.TAG_TOKEN, tagId); + notifyIntent.putExtra(TaskListSubActivity.FROM_NOTIFICATION_TOKEN, true); + PendingIntent pendingIntent = PendingIntent.getActivity(context, + TAG_ID_OFFSET + (int)tagId, notifyIntent, PendingIntent.FLAG_ONE_SHOT); + + // set up properties (name and icon) for the notification + String appName = r.getString(R.string.app_name); + int icon = R.drawable.notif_tag; + + // create notification object + Notification notification = new Notification( + icon, reminder, System.currentTimeMillis()); + notification.setLatestEventInfo(context, + appName, + reminder, + pendingIntent); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.ledARGB = Color.BLUE; + notification.defaults = Notification.DEFAULT_LIGHTS; + + if(quietHours) { + notification.vibrate = null; + notification.sound = null; + } else { + notification.defaults |= Notification.DEFAULT_VIBRATE; + Uri notificationSound = Preferences.getNotificationRingtone(context); + if(notificationSound != null && + !notificationSound.toString().equals("")) { + notification.sound = notificationSound; + } else { + notification.defaults |= Notification.DEFAULT_SOUND; + } + } + + if(Constants.DEBUG) + Log.w("Astrid", "Logging tag notification: " + reminder); + nm.notify(TAG_ID_OFFSET + (int)tagId, notification); + + return true; + } } \ No newline at end of file