diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml index b1d3cb749..1c36008a4 100644 --- a/astrid/AndroidManifest.xml +++ b/astrid/AndroidManifest.xml @@ -1,8 +1,10 @@ + android:versionName="3.1.0" android:versionCode="146" + android:installLocation="internalOnly"> + + @@ -46,7 +48,7 @@ - @@ -163,6 +165,15 @@ + + + + + + + + + @@ -314,7 +325,7 @@ diff --git a/astrid/api-src/com/todoroo/astrid/api/AstridApiConstants.java b/astrid/api-src/com/todoroo/astrid/api/AstridApiConstants.java index 7a284f37b..6cc7f61c5 100644 --- a/astrid/api-src/com/todoroo/astrid/api/AstridApiConstants.java +++ b/astrid/api-src/com/todoroo/astrid/api/AstridApiConstants.java @@ -52,6 +52,16 @@ public class AstridApiConstants { */ public static final String EXTRAS_EXTENDED = "extended"; + /** + * Extras name for old task due date + */ + public static final String EXTRAS_OLD_DUE_DATE= "oldDueDate"; + + /** + * Extras name for new task due date + */ + public static final String EXTRAS_NEW_DUE_DATE = "newDueDate"; + // --- Add-ons API /** @@ -177,10 +187,12 @@ public class AstridApiConstants { public static final String BROADCAST_EVENT_TASK_COMPLETED = PACKAGE + ".TASK_COMPLETED"; /** - * Action name for broadcast intent notifying that task was created + * Action name for broadcast intent notifying that task was created from repeating template * @extra EXTRAS_TASK_ID id of the task + * @extra EXTRAS_OLD_DUE_DATE task old due date (could be 0) + * @extra EXTRAS_NEW_DUE_DATE task new due date (will not be 0) */ - public static final String BROADCAST_EVENT_TASK_CREATED = PACKAGE + ".TASK_CREATED"; + public static final String BROADCAST_EVENT_TASK_REPEATED = PACKAGE + ".TASK_REPEATED"; // --- SQL Constants diff --git a/astrid/common-src/com/todoroo/andlib/sql/Field.java b/astrid/common-src/com/todoroo/andlib/sql/Field.java index 28bbb00b2..c2989f80c 100644 --- a/astrid/common-src/com/todoroo/andlib/sql/Field.java +++ b/astrid/common-src/com/todoroo/andlib/sql/Field.java @@ -61,6 +61,11 @@ public class Field extends DBObject { return UnaryCriterion.like(this, value); } + public Criterion like(String value, String escape) { + return UnaryCriterion.like(this, value, escape); + } + + public Criterion in(final T... value) { final Field field = this; return new Criterion(Operator.in) { diff --git a/astrid/common-src/com/todoroo/andlib/sql/Query.java b/astrid/common-src/com/todoroo/andlib/sql/Query.java index 620087ced..7cc16c459 100644 --- a/astrid/common-src/com/todoroo/andlib/sql/Query.java +++ b/astrid/common-src/com/todoroo/andlib/sql/Query.java @@ -89,14 +89,14 @@ public final class Query { visitSelectClause(sql); visitFromClause(sql); + visitJoinClause(sql); if(queryTemplate == null) { - visitJoinClause(sql); visitWhereClause(sql); visitGroupByClause(sql); visitOrderByClause(sql); visitLimitClause(sql); } else { - if(joins.size() > 0 || groupBies.size() > 0 || orders.size() > 0 || + if(groupBies.size() > 0 || orders.size() > 0 || havings.size() > 0) throw new IllegalStateException("Can't have extras AND query template"); //$NON-NLS-1$ sql.append(queryTemplate); diff --git a/astrid/common-src/com/todoroo/andlib/sql/UnaryCriterion.java b/astrid/common-src/com/todoroo/andlib/sql/UnaryCriterion.java index d7e958440..340b0573c 100644 --- a/astrid/common-src/com/todoroo/andlib/sql/UnaryCriterion.java +++ b/astrid/common-src/com/todoroo/andlib/sql/UnaryCriterion.java @@ -89,4 +89,19 @@ public class UnaryCriterion extends Criterion { } }; } + + public static Criterion like(Field field, String value, final String escape) { + return new UnaryCriterion(field, Operator.like, value) { + @Override + protected void populateOperator(StringBuilder sb) { + sb.append(SPACE).append(operator).append(SPACE); + } + @SuppressWarnings("nls") + @Override + protected void afterPopulateOperator(StringBuilder sb) { + super.afterPopulateOperator(sb); + sb.append(SPACE).append("ESCAPE").append(" '").append(sanitize(escape)).append("'"); + } + }; + } } diff --git a/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java b/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java index e8015658b..b4956414c 100644 --- a/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/astrid/common-src/com/todoroo/andlib/utility/AndroidUtilities.java @@ -193,11 +193,21 @@ public class AndroidUtilities { if(string == null) return null; - String[] pairs = string.split(" "); + String[] pairs = string.split("="); ContentValues result = new ContentValues(); - for(String item : pairs) { - String[] keyValue = item.split("="); - result.put(keyValue[0].trim(), keyValue[1].trim()); + String key = null; + for(int i = 0; i < pairs.length; i++) { + String newKey = null; + int lastSpace = pairs[i].lastIndexOf(' '); + if(lastSpace != -1) { + newKey = pairs[i].substring(lastSpace + 1); + pairs[i] = pairs[i].substring(0, lastSpace); + } else { + newKey = pairs[i]; + } + if(key != null) + result.put(key.trim(), pairs[i].trim()); + key = newKey; } return result; } diff --git a/astrid/common-src/com/todoroo/andlib/utility/DateUtilities.java b/astrid/common-src/com/todoroo/andlib/utility/DateUtilities.java index 2e1e4162a..b1c64eeb4 100644 --- a/astrid/common-src/com/todoroo/andlib/utility/DateUtilities.java +++ b/astrid/common-src/com/todoroo/andlib/utility/DateUtilities.java @@ -6,11 +6,13 @@ package com.todoroo.andlib.utility; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.Locale; import android.content.Context; import android.content.res.Resources; +import android.text.format.DateUtils; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; @@ -108,101 +110,66 @@ public class DateUtilities { } /** - * @return time format (hours and minutes) - */ - public static SimpleDateFormat getTimeFormat(Context context) { - String value = getTimeFormatString(context); - return new SimpleDateFormat(value); - } - - /** - * @return string used for time formatting + * @param context android context + * @param date time to format + * @return time, with hours and minutes */ @SuppressWarnings("nls") - private static String getTimeFormatString(Context context) { + public static String getTimeString(Context context, Date date) { String value; if (is24HourFormat(context)) { value = "H:mm"; } else { value = "h:mm a"; } - return value; + return new SimpleDateFormat(value).format(date); } /** * @param context android context - * @return string used for date formatting + * @param date date to format + * @return date, with month, day, and year */ @SuppressWarnings("nls") - private static String getDateFormatString(Context context) { + public static String getDateString(Context context, Date date) { + String month = "'" + DateUtils.getMonthString(date.getMonth() + + Calendar.JANUARY, DateUtils.LENGTH_MEDIUM) + "'"; String value; // united states, you are special if (Locale.US.equals(Locale.getDefault()) || Locale.CANADA.equals(Locale.getDefault())) - value = "MMM d yyyy"; + value = month + " d yyyy"; else - value = "d MMM yyyy"; - return value; - } - - /** - * @return date format (month, day, year) - */ - @SuppressWarnings("nls") - public static SimpleDateFormat getDateFormat(Context context) { - try { - return new SimpleDateFormat(getDateFormatString(context)); - } catch (Exception e) { - return new SimpleDateFormat("d MMM yyyy"); - } + value = "d " + month + " yyyy"; + return new SimpleDateFormat(value).format(date); } /** * @return date format as getDateFormat with weekday */ @SuppressWarnings("nls") - public static SimpleDateFormat getDateFormatWithWeekday(Context context) { - try { - return new SimpleDateFormat("EEEE, " + getDateFormatString(context)); - } catch (Exception e) { - return new SimpleDateFormat("EEEE, d MMM yyyy"); - } + public static String getDateStringWithWeekday(Context context, Date date) { + String weekday = DateUtils.getDayOfWeekString(date.getDay() + Calendar.SUNDAY, + DateUtils.LENGTH_LONG); + return weekday + ", " + getDateString(context, date); } /** * @return date format as getDateFormat with weekday */ @SuppressWarnings("nls") - public static SimpleDateFormat getDateWithTimeAndWeekday(Context context) { - try { - return new SimpleDateFormat("EEEE, " + getDateFormatString(context) - + " " + getTimeFormatString(context)); - } catch (Exception e) { - return new SimpleDateFormat("EEEE, d MMM yyyy H:mm"); - } + public static String getDateStringWithTimeAndWeekday(Context context, Date date) { + return getDateStringWithWeekday(context, date) + " " + getTimeString(context, date); } /** * @return date with time at the end */ @SuppressWarnings("nls") - public static SimpleDateFormat getDateWithTimeFormat(Context context) { - try { - return new SimpleDateFormat(getDateFormatString(context) + " " + - getTimeFormatString(context)); - } catch (Exception e) { - return new SimpleDateFormat("d MMM yyyy H:mm"); - } + public static String getDateStringWithTime(Context context, Date date) { + return getDateString(context, date) + " " + getTimeString(context, date); } - /** - * @return formatted date (will contain month, day, year) - */ - public static String getFormattedDate(Context context, Date date) { - return getDateFormat(context).format(date); - } - - /* ====================================================================== * ============================================================= duration * ====================================================================== */ diff --git a/astrid/common-src/com/todoroo/andlib/widget/Api4GestureDetector.java b/astrid/common-src/com/todoroo/andlib/widget/Api4GestureDetector.java new file mode 100644 index 000000000..957f81772 --- /dev/null +++ b/astrid/common-src/com/todoroo/andlib/widget/Api4GestureDetector.java @@ -0,0 +1,42 @@ +package com.todoroo.andlib.widget; + +import java.util.ArrayList; + +import android.app.Activity; +import android.gesture.Gesture; +import android.gesture.GestureLibraries; +import android.gesture.GestureLibrary; +import android.gesture.GestureOverlayView; +import android.gesture.Prediction; +import android.gesture.GestureOverlayView.OnGesturePerformedListener; + +import com.todoroo.andlib.widget.GestureService.GestureInterface; + +public class Api4GestureDetector implements OnGesturePerformedListener { + private final GestureLibrary mLibrary; + private final GestureInterface listener; + + public Api4GestureDetector(Activity activity, int view, int gestureLibrary, GestureInterface listener) { + this.listener = listener; + mLibrary = GestureLibraries.fromRawResource(activity, gestureLibrary); + + if(mLibrary.load()) { + GestureOverlayView gestures = (GestureOverlayView) activity.findViewById(view); + gestures.addOnGesturePerformedListener(this); + } + } + + @Override + public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) { + ArrayList predictions = mLibrary.recognize(gesture); + + // We want at least one prediction + if (predictions.size() > 0) { + Prediction prediction = predictions.get(0); + // We want at least some confidence in the result + if (prediction.score > 1.0) { + listener.gesturePerformed(prediction.name); + } + } + } +} \ No newline at end of file diff --git a/astrid/common-src/com/todoroo/andlib/widget/GestureService.java b/astrid/common-src/com/todoroo/andlib/widget/GestureService.java new file mode 100644 index 000000000..128ee2b5e --- /dev/null +++ b/astrid/common-src/com/todoroo/andlib/widget/GestureService.java @@ -0,0 +1,38 @@ +package com.todoroo.andlib.widget; + + +import android.app.Activity; + +import com.todoroo.andlib.utility.AndroidUtilities; + + + + +/** + * All API versions-friendly gesture detector. On SDK < 4, nothing happens + * @author Tim Su + * + */ +public class GestureService { + + public interface GestureInterface { + public void gesturePerformed(String gesture); + } + + /** + * Register gesture detector. If android SDK version is not correct, + * a {@link VerifyError} will be throw. Catch this explicitly. + * + * @param activity + * @param view + * @param gestureLibrary + * @param listener + * @throws VerifyError + */ + public static void registerGestureDetector(Activity activity, int view, + int gestureLibrary, GestureInterface listener) throws VerifyError { + if(AndroidUtilities.getSdkVersion() > 3) + new Api4GestureDetector(activity, view, gestureLibrary, listener); + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/alarms/Alarm.java b/astrid/plugin-src/com/todoroo/astrid/alarms/Alarm.java index 47b1fba67..29e0e7d0a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/alarms/Alarm.java +++ b/astrid/plugin-src/com/todoroo/astrid/alarms/Alarm.java @@ -1,111 +1,34 @@ -/** - * See the file "LICENSE" for the full license governing this code. - */ package com.todoroo.astrid.alarms; - -import android.content.ContentValues; - -import com.todoroo.andlib.data.AbstractModel; -import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property.IntegerProperty; import com.todoroo.andlib.data.Property.LongProperty; -import com.todoroo.andlib.data.Property.StringProperty; -import com.todoroo.andlib.data.Table; -import com.todoroo.andlib.data.TodorooCursor; -import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.model.Metadata; /** - * Data Model which represents an alarm + * Metadata entry for a task alarm * * @author Tim Su * */ -@SuppressWarnings("nls") -public class Alarm extends AbstractModel { - - // --- table - - public static final Table TABLE = new Table("alarm", Alarm.class); +public class Alarm { - // --- properties + /** metadata key */ + public static final String METADATA_KEY = "alarm"; //$NON-NLS-1$ - /** ID */ - public static final LongProperty ID = new LongProperty( - TABLE, ID_PROPERTY_NAME); + /** time of alarm */ + public static final LongProperty TIME = new LongProperty(Metadata.TABLE, + Metadata.VALUE1.name); - /** Associated Task */ - public static final LongProperty TASK = new LongProperty( - TABLE, "task"); - - /** Alarm Time */ - public static final LongProperty TIME = new LongProperty( - TABLE, "time"); - - /** Alarm Type (see constants) */ - public static final IntegerProperty TYPE = new IntegerProperty( - TABLE, "type"); - - /** Alarm Ringtone */ - public static final StringProperty RINGTONE = new StringProperty( - TABLE, "ringtone"); - - /** List of all properties for this model */ - public static final Property[] PROPERTIES = generateProperties(Alarm.class); + /** alarm type */ + public static final IntegerProperty TYPE = new IntegerProperty(Metadata.TABLE, + Metadata.VALUE2.name); // --- constants - /** this alarm was already triggered */ - public static final int TYPE_TRIGGERED = 0; - /** this alarm is single-shot */ public static final int TYPE_SINGLE = 1; /** this alarm repeats itself until turned off */ public static final int TYPE_REPEATING = 2; - // --- defaults - - /** Default values container */ - private static final ContentValues defaultValues = new ContentValues(); - - static { - defaultValues.put(TYPE.name, TYPE_SINGLE); - defaultValues.put(RINGTONE.name, ""); - } - - @Override - public ContentValues getDefaultValues() { - return defaultValues; - } - - // --- data access boilerplate - - public Alarm() { - super(); - } - - public Alarm(TodorooCursor cursor) { - this(); - readPropertiesFromCursor(cursor); - } - - public void readFromCursor(TodorooCursor cursor) { - super.readPropertiesFromCursor(cursor); - } - - @Override - public long getId() { - return getIdHelper(ID); - }; - - // --- parcelable helpers - - private static final Creator CREATOR = new ModelCreator(Task.class); - - @Override - protected Creator getCreator() { - return CREATOR; - } - } diff --git a/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmControlSet.java b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmControlSet.java new file mode 100644 index 000000000..f03ed0aac --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmControlSet.java @@ -0,0 +1,92 @@ +package com.todoroo.astrid.alarms; + +import java.util.Date; +import java.util.LinkedHashSet; + +import android.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import com.timsu.astrid.R; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.widget.DateControlSet; +import com.todoroo.astrid.activity.TaskEditActivity.TaskEditControlSet; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.Task; + +/** + * Control set to manage adding and removing tags + * + * @author Tim Su + * + */ +public final class AlarmControlSet implements TaskEditControlSet { + + // --- instance variables + + private final LinearLayout alertsContainer; + private final Activity activity; + + public AlarmControlSet(Activity activity, ViewGroup parent) { + View v = LayoutInflater.from(activity).inflate(R.layout.alarm_control, parent, true); + + this.activity = activity; + this.alertsContainer = (LinearLayout) v.findViewById(R.id.alert_container); + v.findViewById(R.id.alarms_add).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + addAlarm(new Date()); + } + }); + } + + @Override + public void readFromTask(Task task) { + alertsContainer.removeAllViews(); + TodorooCursor cursor = AlarmService.getInstance().getAlarms(task.getId()); + try { + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) + addAlarm(new Date(cursor.get(Alarm.TIME))); + } finally { + cursor.close(); + } + } + + @Override + public void writeToModel(Task task) { + LinkedHashSet alarms = new LinkedHashSet(); + for(int i = 0; i < alertsContainer.getChildCount(); i++) { + DateControlSet set = (DateControlSet) alertsContainer.getChildAt(i).getTag(); + if(set == null) + continue; + Date date = set.getDate(); + if(date != null) + alarms.add(set.getDate().getTime()); + } + AlarmService.getInstance().synchronizeAlarms(task.getId(), alarms); + } + + private boolean addAlarm(Date alert) { + final View alertItem = LayoutInflater.from(activity).inflate(R.layout.alarm_edit_row, null); + alertsContainer.addView(alertItem); + + DateControlSet dcs = new DateControlSet(activity, (Button)alertItem.findViewById(R.id.date), + (Button)alertItem.findViewById(R.id.time)); + dcs.setDate(alert); + alertItem.setTag(dcs); + + ImageButton reminderRemoveButton; + reminderRemoveButton = (ImageButton)alertItem.findViewById(R.id.button1); + reminderRemoveButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + alertsContainer.removeView(alertItem); + } + }); + + return true; + } +} \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmDetailExposer.java new file mode 100644 index 000000000..b8596da3b --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmDetailExposer.java @@ -0,0 +1,79 @@ +/** + * See the file "LICENSE" for the full license governing this code. + */ +package com.todoroo.astrid.alarms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.text.format.DateUtils; + +import com.timsu.astrid.R; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.api.DetailExposer; +import com.todoroo.astrid.model.Metadata; + +/** + * Exposes Task Detail for tags, i.e. "Tags: frogs, animals" + * + * @author Tim Su + * + */ +public class AlarmDetailExposer extends BroadcastReceiver implements DetailExposer { + + @Override + public void onReceive(Context context, Intent intent) { + // get tags associated with this task + long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + boolean extended = intent.getBooleanExtra(AstridApiConstants.EXTRAS_EXTENDED, false); + String taskDetail = getTaskDetails(context, taskId, extended); + if(taskDetail == null) + return; + + // transmit + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, AlarmService.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 String getTaskDetails(Context context, long id, boolean extended) { + if(extended) + return null; + + TodorooCursor cursor = AlarmService.getInstance().getAlarms(id); + long nextTime = -1; + try { + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + long time = cursor.get(Alarm.TIME); + if(time > DateUtilities.now()) { + nextTime = time; + break; + } + } + + if(nextTime == -1) + return null; + CharSequence durationString = DateUtils.getRelativeDateTimeString(context, + nextTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, + DateUtils.FORMAT_ABBREV_ALL); + return context.getString(R.string.alarm_ADE_detail, durationString); + } finally { + cursor.close(); + } + } + + @Override + public String getPluginIdentifier() { + return AlarmService.IDENTIFIER; + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmService.java b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmService.java index c90c13cf2..a1fa25b84 100644 --- a/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmService.java +++ b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmService.java @@ -1,11 +1,28 @@ package com.todoroo.astrid.alarms; -import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashSet; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; -import com.todoroo.andlib.data.GenericDao; import com.todoroo.andlib.data.TodorooCursor; -import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Join; +import com.todoroo.andlib.sql.Order; import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.core.PluginServices; +import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; +import com.todoroo.astrid.dao.TaskDao.TaskCriteria; +import com.todoroo.astrid.model.Metadata; +import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.reminders.Notifications; +import com.todoroo.astrid.reminders.ReminderService; +import com.todoroo.astrid.service.MetadataService; /** * Provides operations for working with alerts @@ -13,32 +30,31 @@ import com.todoroo.andlib.sql.Query; * @author Tim Su * */ -@SuppressWarnings("nls") public class AlarmService { - AlarmDatabase database = new AlarmDatabase(); - - GenericDao dao = new GenericDao(Alarm.class, database); + // --- singleton - /** - * Metadata key for # of alarms - */ - public static final String ALARM_COUNT = "alarms-count"; + private static AlarmService instance = null; - public AlarmService() { - DependencyInjectionService.getInstance().inject(this); + public static synchronized AlarmService getInstance() { + if(instance == null) + instance = new AlarmService(); + return instance; } + // --- data retrieval + + public static final String IDENTIFIER = "alarms"; //$NON-NLS-1$ /** - * Return alarms for the given task + * Return alarms for the given task. PLEASE CLOSE THE CURSOR! * * @param taskId */ - public TodorooCursor getAlarms(long taskId) { - database.openForReading(); - Query query = Query.select(Alarm.PROPERTIES).where(Alarm.TASK.eq(taskId)); - return dao.query(query); + public TodorooCursor getAlarms(long taskId) { + return PluginServices.getMetadataService().query(Query.select( + Metadata.PROPERTIES).where(MetadataCriteria.byTaskAndwithKey( + taskId, Alarm.METADATA_KEY)).orderBy(Order.asc(Alarm.TIME))); } /** @@ -46,14 +62,140 @@ public class AlarmService { * @param taskId * @param tags */ - public void synchronizeAlarms(long taskId, ArrayList alarms) { - database.openForWriting(); - dao.deleteWhere(Alarm.TASK.eq(taskId)); - - for(Alarm alarm : alarms) { - alarm.setId(Alarm.NO_ID); - alarm.setValue(Alarm.TASK, taskId); - dao.saveExisting(alarm); + public void synchronizeAlarms(long taskId, LinkedHashSet alarms) { + MetadataService service = PluginServices.getMetadataService(); + + if(alarmsIdentical(taskId, alarms)) + return; + + service.deleteWhere(Criterion.and(MetadataCriteria.byTask(taskId), + MetadataCriteria.withKey(Alarm.METADATA_KEY))); + + Metadata metadata = new Metadata(); + metadata.setValue(Metadata.KEY, Alarm.METADATA_KEY); + metadata.setValue(Metadata.TASK, taskId); + for(Long alarm : alarms) { + metadata.clearValue(Metadata.ID); + metadata.setValue(Alarm.TIME, alarm); + metadata.setValue(Alarm.TYPE, Alarm.TYPE_SINGLE); + service.save(metadata); + scheduleAlarm(metadata); + } + } + + private boolean alarmsIdentical(long taskId, LinkedHashSet alarms) { + TodorooCursor cursor = getAlarms(taskId); + try { + if(cursor.getCount() != alarms.size()) + return false; + for(Long alarm : alarms) { + cursor.moveToNext(); + if(alarm != cursor.get(Alarm.TIME)) + return false; + } + } finally { + cursor.close(); + } + + return true; + } + + // --- alarm scheduling + + /** + * Gets a listing of all alarms that are active + * @param properties + * @return todoroo cursor. PLEASE CLOSE THIS CURSOR! + */ + private TodorooCursor getActiveAlarms() { + return PluginServices.getMetadataService().query(Query.select(Alarm.TIME). + join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). + where(Criterion.and(TaskCriteria.isActive(), MetadataCriteria.withKey(Alarm.METADATA_KEY)))); + } + + /** + * Gets a listing of alarms by task + * @param properties + * @return todoroo cursor. PLEASE CLOSE THIS CURSOR! + */ + private TodorooCursor getAlarmsForTask(long taskId) { + return PluginServices.getMetadataService().query(Query.select(Alarm.TIME). + join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). + where(Criterion.and(TaskCriteria.isActive(), + MetadataCriteria.byTaskAndwithKey(taskId, Alarm.METADATA_KEY)))); + } + + /** + * Schedules all alarms + */ + public void scheduleAllAlarms() { + TodorooCursor cursor = getActiveAlarms(); + try { + Metadata alarm = new Metadata(); + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + alarm.readFromCursor(cursor); + scheduleAlarm(alarm); + } + } catch (Exception e) { + // suppress + } finally { + cursor.close(); + } + } + + private static final long NO_ALARM = Long.MAX_VALUE; + + /** + * Schedules alarms for a single task + * @param task + */ + public void scheduleAlarms(Task task) { + TodorooCursor cursor = getAlarmsForTask(task.getId()); + try { + Metadata alarm = new Metadata(); + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + alarm.readFromCursor(cursor); + scheduleAlarm(alarm); + } + } catch (Exception e) { + // suppress + } finally { + cursor.close(); + } + } + + /** + * Schedules alarms for a single task + * + * @param shouldPerformPropertyCheck + * whether to check if task has requisite properties + */ + @SuppressWarnings("nls") + private void scheduleAlarm(Metadata alarm) { + if(alarm == null) + return; + + long taskId = alarm.getValue(Metadata.TASK); + int type = ReminderService.TYPE_ALARM; + + Context context = ContextManager.getContext(); + Intent intent = new Intent(context, Notifications.class); + intent.setType("ALARM" + Long.toString(taskId)); //$NON-NLS-1$ + intent.setAction(Integer.toString(type)); + intent.putExtra(Notifications.ID_KEY, taskId); + intent.putExtra(Notifications.TYPE_KEY, type); + + AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, + intent, 0); + + long time = alarm.getValue(Alarm.TIME); + if(time == 0 || time == NO_ALARM) + am.cancel(pendingIntent); + else { + Log.e("Astrid", "Alarm (" + taskId + ", " + type + + ") set for " + new Date(time)); + am.set(AlarmManager.RTC_WAKEUP, time, pendingIntent); } } } diff --git a/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmTaskRepeatListener.java b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmTaskRepeatListener.java new file mode 100644 index 000000000..6b4696de0 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/alarms/AlarmTaskRepeatListener.java @@ -0,0 +1,47 @@ +package com.todoroo.astrid.alarms; + +import java.util.LinkedHashSet; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.model.Metadata; + +public class AlarmTaskRepeatListener extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); + if(taskId == -1) + return; + + long oldDueDate = intent.getLongExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, 0); + if(oldDueDate == 0) + oldDueDate = DateUtilities.now(); + long newDueDate = intent.getLongExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, -1); + if(newDueDate <= 0 || newDueDate <= oldDueDate) + return; + + TodorooCursor cursor = AlarmService.getInstance().getAlarms(taskId); + try { + if(cursor.getCount() == 0) + return; + + Metadata metadata = new Metadata(); + LinkedHashSet alarms = new LinkedHashSet(cursor.getCount()); + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + metadata.readFromCursor(cursor); + alarms.add(metadata.getValue(Alarm.TIME) + (newDueDate - oldDueDate)); + } + AlarmService.getInstance().synchronizeAlarms(taskId, alarms); + + } finally { + cursor.close(); + } + } + +} diff --git a/astrid/plugin-src/com/todoroo/astrid/backup/BackupPreferences.java b/astrid/plugin-src/com/todoroo/astrid/backup/BackupPreferences.java index 00a3f5de6..edde100e4 100644 --- a/astrid/plugin-src/com/todoroo/astrid/backup/BackupPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/backup/BackupPreferences.java @@ -121,8 +121,8 @@ public class BackupPreferences extends TodorooPreferences { }); } else if(last > 0) { status = r.getString(R.string.backup_status_success, - DateUtilities.getDateWithTimeFormat(BackupPreferences.this). - format(new Date(last))); + DateUtilities.getDateStringWithTime(BackupPreferences.this, + new Date(last))); statusColor = Color.rgb(0, 100, 0); preference.setOnPreferenceClickListener(null); } else { diff --git a/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlExporter.java b/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlExporter.java index a779358be..1829d1c4f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlExporter.java +++ b/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlExporter.java @@ -6,6 +6,7 @@ import java.io.IOException; import org.xmlpull.v1.XmlSerializer; +import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.os.Handler; @@ -16,8 +17,8 @@ import android.widget.Toast; import com.timsu.astrid.R; import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.Property; -import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.Property.PropertyVisitor; +import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.sql.Order; import com.todoroo.andlib.sql.Query; @@ -82,6 +83,8 @@ public class TasksXmlExporter { progressDialog.setCancelable(false); progressDialog.setIndeterminate(false); progressDialog.show(); + if(context instanceof Activity) + progressDialog.setOwnerActivity((Activity)context); } new Thread(new Runnable() { diff --git a/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java b/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java index 8a557692e..da8dea146 100644 --- a/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java +++ b/astrid/plugin-src/com/todoroo/astrid/backup/TasksXmlImporter.java @@ -2,14 +2,15 @@ package com.todoroo.astrid.backup; import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashSet; import java.util.StringTokenizer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; +import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; @@ -97,6 +98,8 @@ public class TasksXmlImporter { progressDialog.setCancelable(false); progressDialog.setIndeterminate(true); progressDialog.show(); + if(context instanceof Activity) + progressDialog.setOwnerActivity((Activity)context); new Thread(new Runnable() { @Override @@ -326,7 +329,7 @@ public class TasksXmlImporter { private String upgradeNotes = null; private boolean syncOnComplete = false; - private final ArrayList tags = new ArrayList(); + private final LinkedHashSet tags = new LinkedHashSet(); public Format1TaskImporter(XmlPullParser xpp) throws XmlPullParserException, IOException { this.xpp = xpp; @@ -496,7 +499,7 @@ public class TasksXmlImporter { if(preferred != null) { Date preferredDate = BackupDateUtilities.getDateFromIso8601String(value); upgradeNotes = "Goal Deadline: " + - DateUtilities.getFormattedDate(ContextManager.getContext(), + DateUtilities.getDateString(ContextManager.getContext(), preferredDate); } task.setValue(Task.DUE_DATE, diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java b/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java index 1cf0b2df4..0dfb33002 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CoreFilterExposer.java @@ -14,6 +14,7 @@ import com.timsu.astrid.R; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Functions; import com.todoroo.andlib.sql.Order; +import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.sql.QueryTemplate; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.activity.FilterListActivity; @@ -22,8 +23,11 @@ import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterCategory; import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.SearchFilter; +import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.dao.TaskDao.TaskCriteria; +import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Task; +import com.todoroo.astrid.tags.TagService; /** * Exposes Astrid's built in filters to the {@link FilterListActivity} @@ -86,7 +90,7 @@ public final class CoreFilterExposer extends BroadcastReceiver { r.getString(R.string.BFE_Alphabetical), new QueryTemplate().where(Criterion.and(TaskCriteria.isActive(), TaskCriteria.isVisible())). - orderBy(Order.asc(Task.TITLE)), + orderBy(Order.asc(Functions.upper(Task.TITLE))), null); alphabetical.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.tango_alpha)).getBitmap(); @@ -134,8 +138,11 @@ public final class CoreFilterExposer extends BroadcastReceiver { */ public static Filter buildInboxFilter(Resources r) { Filter inbox = new Filter(r.getString(R.string.BFE_Active), r.getString(R.string.BFE_Active_title), - new QueryTemplate().where(Criterion.and(TaskCriteria.isActive(), - TaskCriteria.isVisible())), + new QueryTemplate().where( + Criterion.and(TaskCriteria.isActive(), TaskCriteria.isVisible(), + Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).where( + Criterion.and(MetadataCriteria.withKey(TagService.KEY), + TagService.TAG.like("x_%", "x"))))))), //$NON-NLS-1$ //$NON-NLS-2$ null); inbox.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.tango_home)).getBitmap(); return inbox; diff --git a/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java b/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java index f5a1b0499..83543a1bd 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/PluginServices.java @@ -3,9 +3,11 @@ package com.todoroo.astrid.core; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.service.AddOnService; +import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.TaskService; @@ -32,11 +34,18 @@ public final class PluginServices { @Autowired DialogUtilities dialogUtilities; + @Autowired + DateUtilities dateUtilities; + @Autowired AddOnService addOnService; private static PluginServices instance; + static { + AstridDependencyInjector.initialize(); + } + private PluginServices() { DependencyInjectionService.getInstance().inject(this); } @@ -68,4 +77,8 @@ public final class PluginServices { public static DialogUtilities getDialogUtilities() { return getInstance().dialogUtilities; } + + public static DateUtilities getDateUtilities() { + return getInstance().dateUtilities; + } } diff --git a/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java b/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java index d8bdfeddf..555fd2517 100644 --- a/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java +++ b/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java @@ -16,6 +16,7 @@ import android.widget.Spinner; import com.flurry.android.FlurryAgent; import com.timsu.astrid.R; +import com.todoroo.andlib.service.ContextManager; import com.todoroo.astrid.activity.AddOnActivity; import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.api.Filter; @@ -96,6 +97,8 @@ public final class LocaleEditAlerts extends ExpandableListActivity { super.onCreate(savedInstanceState); setContentView(R.layout.locale_edit_alerts); + ContextManager.setContext(this); + /* * Locale guarantees that the breadcrumb string will be present, but checking for null anyway makes your Activity more * robust and re-usable diff --git a/astrid/plugin-src/com/todoroo/astrid/locale/LocaleReceiver.java b/astrid/plugin-src/com/todoroo/astrid/locale/LocaleReceiver.java index bdfc6b87f..5d5a03c5c 100644 --- a/astrid/plugin-src/com/todoroo/astrid/locale/LocaleReceiver.java +++ b/astrid/plugin-src/com/todoroo/astrid/locale/LocaleReceiver.java @@ -9,7 +9,7 @@ import android.util.Log; import com.timsu.astrid.R; import com.todoroo.andlib.data.TodorooCursor; -import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.activity.ShortcutActivity; @@ -17,7 +17,6 @@ import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.model.Task; import com.todoroo.astrid.reminders.Notifications; -import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Preferences; @@ -29,9 +28,6 @@ import com.todoroo.astrid.utility.Preferences; */ public class LocaleReceiver extends BroadcastReceiver { - @Autowired - private TaskService taskService; - /** * Create a preference key for storing / retrieving last interval time * @param filterTitle @@ -46,6 +42,8 @@ public class LocaleReceiver extends BroadcastReceiver { @Override /** Called when the system is started up */ public void onReceive(Context context, Intent intent) { + ContextManager.setContext(context); + try { if (com.twofortyfouram.Intent.ACTION_FIRE_SETTING.equals(intent.getAction())) { if(!PluginServices.getAddOnService().hasLocalePlugin()) @@ -72,7 +70,7 @@ public class LocaleReceiver extends BroadcastReceiver { DependencyInjectionService.getInstance().inject(this); Filter filter = new Filter(title, title, null, null); filter.sqlQuery = sql; - TodorooCursor cursor = taskService.fetchFiltered(filter, null, Task.ID); + TodorooCursor cursor = PluginServices.getTaskService().fetchFiltered(filter, null, Task.ID); try { if(cursor.getCount() == 0) return; diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java b/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java index ce9adf6d3..18ee80574 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java @@ -32,10 +32,10 @@ public class Notifications extends BroadcastReceiver { // --- constants /** task id extra */ - static final String ID_KEY = "id"; //$NON-NLS-1$ + public static final String ID_KEY = "id"; //$NON-NLS-1$ /** notification type extra */ - static final String TYPE_KEY = "type"; //$NON-NLS-1$ + public static final String TYPE_KEY = "type"; //$NON-NLS-1$ /** preference values */ public static final int ICON_SET_PINK = 0; @@ -72,15 +72,24 @@ public class Notifications extends BroadcastReceiver { Resources r = context.getResources(); String reminder; - if(type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE) - reminder = getRandomReminder(r.getStringArray(R.array.reminders_due)); - else if(type == ReminderService.TYPE_SNOOZE) - reminder = getRandomReminder(r.getStringArray(R.array.reminders_snooze)); - else if(Preferences.getBoolean(R.string.p_rmd_nagging, true)) - reminder = getRandomReminder(r.getStringArray(R.array.reminders)); - else + + if(type == ReminderService.TYPE_ALARM) + reminder = getRandomReminder(r.getStringArray(R.array.reminders_alarm)); + else if(Preferences.getBoolean(R.string.p_rmd_nagging, true)) { + if(type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE) + reminder = getRandomReminder(r.getStringArray(R.array.reminders_due)); + else if(type == ReminderService.TYPE_SNOOZE) + reminder = getRandomReminder(r.getStringArray(R.array.reminders_snooze)); + else + reminder = getRandomReminder(r.getStringArray(R.array.reminders)); + } else reminder = ""; //$NON-NLS-1$ + synchronized(Notifications.class) { + if(notificationManager == null) + notificationManager = new AndroidNotificationManager(context); + } + if(!showTaskNotification(id, type, reminder)) { notificationManager.cancel((int)id); } diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderService.java b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderService.java index ce37ae19d..ca4b04580 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderService.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderService.java @@ -48,13 +48,15 @@ public final class ReminderService { }; /** flag for due date reminder */ - static final int TYPE_DUE = 0; + public static final int TYPE_DUE = 0; /** flag for overdue reminder */ - static final int TYPE_OVERDUE = 1; + public static final int TYPE_OVERDUE = 1; /** flag for random reminder */ - static final int TYPE_RANDOM = 2; + public static final int TYPE_RANDOM = 2; /** flag for a snoozed reminder */ - static final int TYPE_SNOOZE = 3; + public static final int TYPE_SNOOZE = 3; + /** flag for an alarm reminder */ + public static final int TYPE_ALARM = 4; static final Random random = new Random(); @@ -179,6 +181,8 @@ public final class ReminderService { scheduler.createAlarm(task, whenDueDate, TYPE_DUE); else if(whenOverdue != NO_ALARM) scheduler.createAlarm(task, whenOverdue, TYPE_OVERDUE); + else + scheduler.createAlarm(task, 0, 0); } /** @@ -291,14 +295,6 @@ public final class ReminderService { */ @SuppressWarnings("nls") public void createAlarm(Task task, long time, int type) { - if(time == 0 || time == NO_ALARM) - return; - - if(time < DateUtilities.now()) { - time = DateUtilities.now() + (long)((0.5f + - 4 * random.nextFloat()) * DateUtilities.ONE_HOUR); - } - Context context = ContextManager.getContext(); Intent intent = new Intent(context, Notifications.class); intent.setType(Long.toString(task.getId())); @@ -310,9 +306,18 @@ public final class ReminderService { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); - Log.e("Astrid", "Alarm (" + task.getId() + ", " + type + - ") set for " + new Date(time)); - am.set(AlarmManager.RTC_WAKEUP, time, pendingIntent); + if(time == 0 || time == NO_ALARM) + am.cancel(pendingIntent); + else { + if(time < DateUtilities.now()) { + time = DateUtilities.now() + (long)((0.5f + + 4 * random.nextFloat()) * DateUtilities.ONE_HOUR); + } + + Log.e("Astrid", "Alarm (" + task.getId() + ", " + type + + ") set for " + new Date(time)); + am.set(AlarmManager.RTC_WAKEUP, time, pendingIntent); + } } } diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderStartupReceiver.java b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderStartupReceiver.java index f76aa32d2..a210bbed0 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderStartupReceiver.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderStartupReceiver.java @@ -8,6 +8,7 @@ import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.ExceptionService; +import com.todoroo.astrid.alarms.AlarmService; import com.todoroo.astrid.service.AstridDependencyInjector; /** @@ -33,6 +34,7 @@ public class ReminderStartupReceiver extends BroadcastReceiver { ContextManager.setContext(context); try { ReminderService.getInstance().scheduleAllAlarms(); + AlarmService.getInstance().scheduleAllAlarms(); } catch (Exception e) { DependencyInjectionService.getInstance().inject(this); exceptionService.reportError("reminder-startup", e); //$NON-NLS-1$ diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java index 8b81b7f17..cf4bddc5c 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java @@ -4,6 +4,7 @@ import java.text.DateFormatSymbols; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; +import java.util.Date; import android.app.Activity; import android.view.LayoutInflater; @@ -11,13 +12,13 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.LinearLayout; import android.widget.Spinner; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.CompoundButton.OnCheckedChangeListener; import com.google.ical.values.Frequency; import com.google.ical.values.RRule; @@ -61,10 +62,13 @@ public class RepeatControlSet implements TaskEditControlSet { private final LinearLayout repeatContainer; private final LinearLayout daysOfWeekContainer; private final CompoundButton[] daysOfWeek = new CompoundButton[7]; + private Task model; @Autowired ExceptionService exceptionService; + boolean setInterval = false; + // --- implementation public RepeatControlSet(final Activity activity, ViewGroup parent) { @@ -111,10 +115,22 @@ public class RepeatControlSet implements TaskEditControlSet { repeatValueClick(); } }); + interval.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parentView, View view, int position, long id) { daysOfWeekContainer.setVisibility(position == INTERVAL_WEEKS ? View.VISIBLE : View.GONE); + if(position == INTERVAL_WEEKS) { + Date date; + if(model.getValue(Task.DUE_DATE) == 0) + date = new Date(); + else + date = new Date(model.getValue(Task.DUE_DATE)); + + int dayOfWeek = date.getDay(); + for(int i = 0; i < 7; i++) + daysOfWeek[i].setChecked(i == dayOfWeek); + } } @Override @@ -122,6 +138,7 @@ public class RepeatControlSet implements TaskEditControlSet { // } }); + daysOfWeekContainer.setVisibility(View.GONE); } /** Set up the repeat value button */ @@ -156,6 +173,8 @@ public class RepeatControlSet implements TaskEditControlSet { @Override public void readFromTask(Task task) { + model = task; + String recurrence = task.getValue(Task.RECURRENCE); if(recurrence == null) recurrence = ""; //$NON-NLS-1$ @@ -172,17 +191,6 @@ public class RepeatControlSet implements TaskEditControlSet { break; case WEEKLY: { interval.setSelection(INTERVAL_WEEKS); - - // clear all day of week checks, then update them - for(int i = 0; i < 7; i++) - daysOfWeek[i].setChecked(false); - - for(WeekdayNum day : rrule.getByDay()) { - for(int i = 0; i < 7; i++) - if(daysOfWeek[i].getTag() == day.wday) - daysOfWeek[i].setChecked(true); - } - break; } case MONTHLY: @@ -196,6 +204,19 @@ public class RepeatControlSet implements TaskEditControlSet { exceptionService.reportError("repeat-unhandled-rule", //$NON-NLS-1$ new Exception("Unhandled rrule frequency: " + recurrence)); //$NON-NLS-1$ } + + // clear all day of week checks, then update them + for(int i = 0; i < 7; i++) + daysOfWeek[i].setChecked(false); + + for(WeekdayNum day : rrule.getByDay()) { + for(int i = 0; i < 7; i++) + if(daysOfWeek[i].getTag() == day.wday) + daysOfWeek[i].setChecked(true); + } + + // suppress first call to interval.onItemSelected + setInterval = true; } catch (ParseException e) { recurrence = ""; //$NON-NLS-1$ exceptionService.reportError("repeat-parse-exception", e); //$NON-NLS-1$ @@ -226,6 +247,7 @@ public class RepeatControlSet implements TaskEditControlSet { break; case INTERVAL_WEEKS: { rrule.setFreq(Frequency.WEEKLY); + ArrayList days = new ArrayList(); for(int i = 0; i < daysOfWeek.length; i++) if(daysOfWeek[i].isChecked()) @@ -239,6 +261,7 @@ public class RepeatControlSet implements TaskEditControlSet { case INTERVAL_HOURS: rrule.setFreq(Frequency.HOURLY); } + result = rrule.toIcal(); } task.setValue(Task.RECURRENCE, result); diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java index 46d3af6f7..4d332b583 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java @@ -92,8 +92,8 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo } interval = "" + interval + ""; //$NON-NLS-1$//$NON-NLS-2$ - if(rrule.getFreq() == Frequency.WEEKLY) { - List byDay = rrule.getByDay(); + List byDay = rrule.getByDay(); + if(rrule.getFreq() == Frequency.WEEKLY || byDay.size() != 7) { if(byDay.size() > 0) { StringBuilder byDayString = new StringBuilder(); DateFormatSymbols dfs = new DateFormatSymbols(); diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java index ac17ffba7..f1934471e 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java @@ -1,6 +1,7 @@ package com.todoroo.astrid.repeats; import java.text.ParseException; +import java.util.Collections; import java.util.Date; import java.util.TimeZone; @@ -15,107 +16,134 @@ import com.google.ical.values.DateValue; import com.google.ical.values.DateValueImpl; import com.google.ical.values.Frequency; import com.google.ical.values.RRule; -import com.todoroo.andlib.service.Autowired; -import com.todoroo.andlib.service.DependencyInjectionService; -import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.api.AstridApiConstants; +import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.model.Task; -import com.todoroo.astrid.service.TaskService; public class RepeatTaskCompleteListener extends BroadcastReceiver { - @Autowired - private TaskService taskService; - - @Autowired - private ExceptionService exceptionService; - @Override public void onReceive(Context context, Intent intent) { long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1); if(taskId == -1) return; - DependencyInjectionService.getInstance().inject(this); - - Task task = taskService.fetchById(taskId, Task.ID, Task.RECURRENCE, + Task task = PluginServices.getTaskService().fetchById(taskId, Task.ID, Task.RECURRENCE, Task.DUE_DATE, Task.FLAGS, Task.HIDE_UNTIL); if(task == null) return; String recurrence = task.getValue(Task.RECURRENCE); if(recurrence != null && recurrence.length() > 0) { - DateValue repeatFrom; - Date repeatFromDate = new Date(); - - DateValue today = new DateValueImpl(repeatFromDate.getYear() + 1900, - repeatFromDate.getMonth() + 1, repeatFromDate.getDate()); - if(task.hasDueDate() && !task.getFlag(Task.FLAGS, Task.FLAG_REPEAT_AFTER_COMPLETION)) { - repeatFromDate = new Date(task.getValue(Task.DUE_DATE)); - if(task.hasDueTime()) { - repeatFrom = new DateTimeValueImpl(repeatFromDate.getYear() + 1900, - repeatFromDate.getMonth() + 1, repeatFromDate.getDate(), - repeatFromDate.getHours(), repeatFromDate.getMinutes(), repeatFromDate.getSeconds()); - } else { - repeatFrom = new DateValueImpl(repeatFromDate.getYear() + 1900, - repeatFromDate.getMonth() + 1, repeatFromDate.getDate()); - } + long newDueDate; + try { + newDueDate = computeNextDueDate(task, recurrence); + if(newDueDate == -1) + return; + } catch (ParseException e) { + PluginServices.getExceptionService().reportError("repeat-parse", e); //$NON-NLS-1$ + return; + } + + long hideUntil = task.getValue(Task.HIDE_UNTIL); + if(hideUntil > 0 && task.getValue(Task.DUE_DATE) > 0) { + hideUntil += newDueDate - task.getValue(Task.DUE_DATE); + } + + // clone to create new task + Task clone = PluginServices.getTaskService().clone(task); + clone.setValue(Task.DUE_DATE, newDueDate); + clone.setValue(Task.HIDE_UNTIL, hideUntil); + clone.setValue(Task.COMPLETION_DATE, 0L); + clone.setValue(Task.TIMER_START, 0L); + clone.setValue(Task.ELAPSED_SECONDS, 0); + PluginServices.getTaskService().save(clone, false); + + // clear recurrence from completed task so it can be re-completed + task.setValue(Task.RECURRENCE, ""); //$NON-NLS-1$ + PluginServices.getTaskService().save(task, false); + + // send a broadcast + Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_REPEATED); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, clone.getId()); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, task.getValue(Task.DUE_DATE)); + broadcastIntent.putExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, newDueDate); + context.sendOrderedBroadcast(broadcastIntent, null); + } + } + + public static long computeNextDueDate(Task task, String recurrence) throws ParseException { + DateValue repeatFrom; + Date repeatFromDate = new Date(); + + DateValue today = new DateValueImpl(repeatFromDate.getYear() + 1900, + repeatFromDate.getMonth() + 1, repeatFromDate.getDate()); + if(task.hasDueDate() && !task.getFlag(Task.FLAGS, Task.FLAG_REPEAT_AFTER_COMPLETION)) { + repeatFromDate = new Date(task.getValue(Task.DUE_DATE)); + if(task.hasDueTime()) { + repeatFrom = new DateTimeValueImpl(repeatFromDate.getYear() + 1900, + repeatFromDate.getMonth() + 1, repeatFromDate.getDate(), + repeatFromDate.getHours(), repeatFromDate.getMinutes(), repeatFromDate.getSeconds()); } else { - repeatFrom = today; + repeatFrom = new DateValueImpl(repeatFromDate.getYear() + 1900, + repeatFromDate.getMonth() + 1, repeatFromDate.getDate()); } + } else { + repeatFrom = today; + } - // invoke the recurrence iterator - try { - long newDueDate; - RRule rrule = new RRule(recurrence); - if(rrule.getFreq() == Frequency.HOURLY) { + // invoke the recurrence iterator + long newDueDate = -1; + RRule rrule = new RRule(recurrence); + + // handle the iCalendar "byDay" field differently depending on if + // we are weekly or otherwise + + if(rrule.getFreq() != Frequency.WEEKLY) { + rrule.setByDay(Collections.EMPTY_LIST); + } + + if(rrule.getFreq() == Frequency.HOURLY) { + newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, + repeatFromDate.getTime() + DateUtilities.ONE_HOUR * rrule.getInterval()); + } else { + RecurrenceIterator iterator = RecurrenceIteratorFactory.createRecurrenceIterator(rrule, + repeatFrom, TimeZone.getDefault()); + DateValue nextDate = repeatFrom; + if(repeatFrom.compareTo(today) < 0) + iterator.advanceTo(today); + + for(int i = 0; i < 10; i++) { // ten tries then we give up + if(!iterator.hasNext()) + return -1; + nextDate = iterator.next(); + + if(nextDate.compareTo(repeatFrom) == 0) + continue; + + if(nextDate instanceof DateTimeValueImpl) { + DateTimeValueImpl newDateTime = (DateTimeValueImpl)nextDate; newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, - repeatFromDate.getTime() + DateUtilities.ONE_HOUR * rrule.getInterval()); + Date.UTC(newDateTime.year() - 1900, newDateTime.month() - 1, + newDateTime.day(), newDateTime.hour(), + newDateTime.minute(), newDateTime.second())); } else { - RecurrenceIterator iterator = RecurrenceIteratorFactory.createRecurrenceIterator(rrule, - repeatFrom, TimeZone.getDefault()); - DateValue nextDate; - if(repeatFrom.compareTo(today) < 0) { - iterator.advanceTo(today); - if(!iterator.hasNext()) - return; - nextDate = iterator.next(); - } else { - iterator.advanceTo(repeatFrom); - if(!iterator.hasNext()) - return; - nextDate = iterator.next(); - nextDate = iterator.next(); - } - - if(nextDate instanceof DateTimeValueImpl) { - DateTimeValueImpl newDateTime = (DateTimeValueImpl)nextDate; - newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, - Date.UTC(newDateTime.year() - 1900, newDateTime.month() - 1, - newDateTime.day(), newDateTime.hour(), - newDateTime.minute(), newDateTime.second())); - } else { - newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, - new Date(nextDate.year() - 1900, nextDate.month() - 1, - nextDate.day()).getTime()); - } + newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, + new Date(nextDate.year() - 1900, nextDate.month() - 1, + nextDate.day()).getTime()); } - long hideUntil = task.getValue(Task.HIDE_UNTIL); - if(hideUntil > 0 && task.getValue(Task.DUE_DATE) > 0) { - hideUntil += newDueDate - task.getValue(Task.DUE_DATE); - } + if(newDueDate > DateUtilities.now() && newDueDate != repeatFromDate.getTime()) + break; - task = taskService.clone(task); - task.setValue(Task.DUE_DATE, newDueDate); - task.setValue(Task.HIDE_UNTIL, hideUntil); - task.setValue(Task.COMPLETION_DATE, 0L); - taskService.save(task, false); - } catch (ParseException e) { - exceptionService.reportError("recurrence-rule: " + recurrence, e); //$NON-NLS-1$ } } + + if(newDueDate == -1) + return -1; + + return newDueDate; } } diff --git a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java index 1f0dc5724..fff43d9d1 100644 --- a/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/rmilk/MilkPreferences.java @@ -114,12 +114,12 @@ public class MilkPreferences extends TodorooPreferences { // last sync was error else if(MilkUtilities.getLastAttemptedSyncDate() != 0) { status = r.getString(R.string.rmilk_status_failed, - DateUtilities.getDateWithTimeFormat(MilkPreferences.this). - format(new Date(MilkUtilities.getLastAttemptedSyncDate()))); + DateUtilities.getDateStringWithTime(MilkPreferences.this, + new Date(MilkUtilities.getLastAttemptedSyncDate()))); if(MilkUtilities.getLastSyncDate() > 0) { subtitle = r.getString(R.string.rmilk_status_failed_subtitle, - DateUtilities.getDateWithTimeFormat(MilkPreferences.this). - format(new Date(MilkUtilities.getLastSyncDate()))); + DateUtilities.getDateStringWithTime(MilkPreferences.this, + new Date(MilkUtilities.getLastSyncDate()))); } statusColor = Color.rgb(100, 0, 0); preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { @@ -132,8 +132,8 @@ public class MilkPreferences extends TodorooPreferences { }); } else if(MilkUtilities.getLastSyncDate() > 0) { status = r.getString(R.string.rmilk_status_success, - DateUtilities.getDateWithTimeFormat(MilkPreferences.this). - format(new Date(MilkUtilities.getLastSyncDate()))); + DateUtilities.getDateStringWithTime(MilkPreferences.this, + new Date(MilkUtilities.getLastSyncDate()))); statusColor = Color.rgb(0, 100, 0); } else { status = r.getString(R.string.rmilk_status_never); diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java index e75248814..7d2276fcc 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagFilterExposer.java @@ -11,12 +11,14 @@ import android.content.res.Resources; import android.graphics.drawable.BitmapDrawable; import com.timsu.astrid.R; +import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.QueryTemplate; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterCategory; import com.todoroo.astrid.api.FilterListHeader; import com.todoroo.astrid.api.FilterListItem; +import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.tags.TagService.Tag; @@ -31,11 +33,11 @@ public class TagFilterExposer extends BroadcastReceiver { private TagService tagService; @SuppressWarnings("nls") - private Filter filterFromTag(Context context, Tag tag) { + private Filter filterFromTag(Context context, Tag tag, Criterion criterion) { String listTitle = context.getString(R.string.tag_FEx_tag_w_size). replace("$T", tag.tag).replace("$C", Integer.toString(tag.count)); String title = context.getString(R.string.tag_FEx_name, tag.tag); - QueryTemplate tagTemplate = tag.queryTemplate(); + QueryTemplate tagTemplate = tag.queryTemplate(criterion); ContentValues contentValues = new ContentValues(); contentValues.put(Metadata.KEY.name, TagService.KEY); contentValues.put(TagService.TAG.name, tag.tag); @@ -59,7 +61,7 @@ public class TagFilterExposer extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { tagService = TagService.getInstance(); - Tag[] tagsByAlpha = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_ALPHA); + Tag[] tagsByAlpha = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_ALPHA, TaskCriteria.notDeleted()); // If user does not have any tags, don't show this section at all if(tagsByAlpha.length == 0) @@ -67,34 +69,47 @@ public class TagFilterExposer extends BroadcastReceiver { Resources r = context.getResources(); - Tag[] tagsBySize = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE); Filter[] filtersByAlpha = new Filter[tagsByAlpha.length]; for(int i = 0; i < tagsByAlpha.length; i++) - filtersByAlpha[i] = filterFromTag(context, tagsByAlpha[i]); + filtersByAlpha[i] = filterFromTag(context, tagsByAlpha[i], TaskCriteria.notDeleted()); + Tag[] tagsBySize = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE, TaskCriteria.isActive()); Filter[] filtersBySize = new Filter[tagsBySize.length]; for(int i = 0; i < tagsBySize.length; i++) - filtersBySize[i] = filterFromTag(context, tagsBySize[i]); + filtersBySize[i] = filterFromTag(context, tagsBySize[i], TaskCriteria.isActive()); + + Tag[] completed = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE, TaskCriteria.completed()); + Filter[] filtersCompleted = new Filter[completed.length]; + for(int i = 0; i < completed.length; i++) + filtersCompleted[i] = filterFromTag(context, completed[i], TaskCriteria.completed()); FilterListHeader tagsHeader = new FilterListHeader(context.getString(R.string.tag_FEx_header)); + Filter untagged = new Filter(r.getString(R.string.tag_FEx_untagged), r.getString(R.string.tag_FEx_untagged), tagService.untaggedTemplate(), null); untagged.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_untagged)).getBitmap(); + FilterCategory tagsCategoryBySize = new FilterCategory(context.getString(R.string.tag_FEx_by_size), filtersBySize); tagsCategoryBySize.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_tags1)).getBitmap(); - FilterCategory tagsCategoryByAlpha = new FilterCategory(context.getString(R.string.tag_FEx_alpha), + + FilterCategory tagsCategoryAllByAlpha = new FilterCategory(context.getString(R.string.tag_FEx_alpha), filtersByAlpha); - tagsCategoryByAlpha.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_tags2)).getBitmap(); + tagsCategoryAllByAlpha.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_tags1)).getBitmap(); + + FilterCategory tagsCategoryCompleted = new FilterCategory(context.getString(R.string.tag_FEx_completed), + filtersCompleted); + tagsCategoryCompleted.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_tags2)).getBitmap(); // transmit filter list - FilterListItem[] list = new FilterListItem[4]; + FilterListItem[] list = new FilterListItem[5]; list[0] = tagsHeader; list[1] = untagged; list[2] = tagsCategoryBySize; - list[3] = tagsCategoryByAlpha; + list[3] = tagsCategoryCompleted; + list[4] = tagsCategoryAllByAlpha; Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS); broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, list); context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ); diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java index 9f73ba6fc..24e912ff1 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java @@ -1,6 +1,6 @@ package com.todoroo.astrid.tags; -import java.util.ArrayList; +import java.util.LinkedHashSet; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.data.Property.CountProperty; @@ -82,11 +82,11 @@ public final class TagService { * @param tag * @return */ - public QueryTemplate queryTemplate() { + public QueryTemplate queryTemplate(Criterion criterion) { return new QueryTemplate().join(Join.inner(Metadata.TABLE, Task.ID.eq(Metadata.TASK))).where(Criterion.and( MetadataCriteria.withKey(KEY), TAG.eq(tag), - TaskCriteria.isActive())); + criterion)); } } @@ -100,13 +100,14 @@ public final class TagService { /** * Return all tags ordered by given clause * - * @param taskId + * @param order ordering + * @param activeStatus criterion for specifying completed or uncompleted * @return empty array if no tags, otherwise array */ - public Tag[] getGroupedTags(Order order) { + public Tag[] getGroupedTags(Order order, Criterion activeStatus) { Query query = Query.select(TAG.as(TAG.name), COUNT). join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). - where(Criterion.and(TaskCriteria.isActive(), MetadataCriteria.withKey(KEY))). + where(Criterion.and(activeStatus, MetadataCriteria.withKey(KEY))). orderBy(order).groupBy(TAG); TodorooCursor cursor = metadataDao.query(query); try { @@ -175,7 +176,7 @@ public final class TagService { * @param taskId * @param tags */ - public void synchronizeTags(long taskId, ArrayList tags) { + public void synchronizeTags(long taskId, LinkedHashSet tags) { metadataDao.deleteWhere(Criterion.and(MetadataCriteria.byTask(taskId), MetadataCriteria.withKey(KEY))); diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagsControlSet.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagsControlSet.java index 0081a545a..da7a11926 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/TagsControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagsControlSet.java @@ -1,12 +1,11 @@ package com.todoroo.astrid.tags; -import java.util.ArrayList; +import java.util.LinkedHashSet; import android.app.Activity; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.ImageButton; @@ -15,6 +14,7 @@ import android.widget.TextView; import com.timsu.astrid.R; import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.sql.Criterion; import com.todoroo.astrid.activity.TaskEditActivity.TaskEditControlSet; import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Task; @@ -28,11 +28,6 @@ import com.todoroo.astrid.tags.TagService.Tag; */ public final class TagsControlSet implements TaskEditControlSet { - // --- constants - - /** Number of tags a task can have */ - static final int MAX_TAGS = 5; - // --- instance variables private final TagService tagService = TagService.getInstance(); @@ -41,30 +36,35 @@ public final class TagsControlSet implements TaskEditControlSet { private final Activity activity; public TagsControlSet(Activity activity, int tagsContainer) { - allTags = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE); + allTags = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE, Criterion.all); this.activity = activity; this.tagsContainer = (LinearLayout) activity.findViewById(tagsContainer); } - @SuppressWarnings("nls") @Override public void readFromTask(Task task) { - // tags (only configure if not already set) - if(tagsContainer.getChildCount() == 0) { - TodorooCursor cursor = tagService.getTags(task.getId()); - try { - for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) - addTag(cursor.get(TagService.TAG)); - } finally { - cursor.close(); - } - addTag(""); + System.err.println("TAGS loading... old size = " + tagsContainer.getChildCount()); + tagsContainer.removeAllViews(); + + TodorooCursor cursor = tagService.getTags(task.getId()); + try { + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) + addTag(cursor.get(TagService.TAG)); + } finally { + cursor.close(); } + if(tagsContainer.getChildCount() == 0) + addTag(""); //$NON-NLS-1$ + System.err.println("TAGS loaded "); } @Override public void writeToModel(Task task) { - ArrayList tags = new ArrayList(); + // this is a case where we're asked to save but the UI was not yet populated + if(tagsContainer.getChildCount() == 0) + return; + + LinkedHashSet tags = new LinkedHashSet(); for(int i = 0; i < tagsContainer.getChildCount(); i++) { TextView tagName = (TextView)tagsContainer.getChildAt(i).findViewById(R.id.text1); @@ -74,14 +74,12 @@ public final class TagsControlSet implements TaskEditControlSet { } tagService.synchronizeTags(task.getId(), tags); + System.err.println("TAGS saved " + tags); } /** Adds a tag to the tag field */ boolean addTag(String tagName) { - if (tagsContainer.getChildCount() >= MAX_TAGS) { - return false; - } - + System.err.println("TAG ADDING ui " + tagName); LayoutInflater inflater = activity.getLayoutInflater(); final View tagItem = inflater.inflate(R.layout.tag_edit_row, null); tagsContainer.addView(tagItem); @@ -93,33 +91,40 @@ public final class TagsControlSet implements TaskEditControlSet { new ArrayAdapter(activity, android.R.layout.simple_dropdown_item_1line, allTags); textView.setAdapter(tagsAdapter); - textView.addTextChangedListener(new TextWatcher() { - @SuppressWarnings("nls") - public void onTextChanged(CharSequence s, int start, int before, - int count) { - if(start == 0 && tagsContainer.getChildAt( - tagsContainer.getChildCount()-1) == tagItem) { - addTag(""); - } - } - public void afterTextChanged(Editable s) { - // + textView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + View lastItem = tagsContainer.getChildAt(tagsContainer.getChildCount()-1); + TextView lastText = (TextView) lastItem.findViewById(R.id.text1); + if(lastText.getText().length() != 0) { + addTag(""); //$NON-NLS-1$ + } } + }); - - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - // + /*textView.setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView arg0, int actionId, KeyEvent arg2) { + if(actionId != EditorInfo.IME_NULL) + return false; + View lastItem = tagsContainer.getChildAt(tagsContainer.getChildCount()-1); + TextView lastText = (TextView) lastItem.findViewById(R.id.text1); + if(lastText.getText().length() != 0) { + addTag(""); //$NON-NLS-1$ + } + return true; } - }); + });*/ ImageButton reminderRemoveButton; reminderRemoveButton = (ImageButton)tagItem.findViewById(R.id.button1); reminderRemoveButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - if(textView.getText().length() > 0) + if(tagsContainer.getChildCount() > 0) tagsContainer.removeView(tagItem); + else + textView.setText(""); //$NON-NLS-1$ } }); diff --git a/astrid/plugin-src/com/todoroo/astrid/timers/TimerControlSet.java b/astrid/plugin-src/com/todoroo/astrid/timers/TimerControlSet.java index d87b3b3e1..866fee76d 100644 --- a/astrid/plugin-src/com/todoroo/astrid/timers/TimerControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/timers/TimerControlSet.java @@ -10,7 +10,6 @@ import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.astrid.activity.TaskEditActivity.TaskEditControlSet; import com.todoroo.astrid.model.Task; import com.todoroo.astrid.ui.TimeDurationControlSet; -import com.todoroo.astrid.ui.TimeDurationControlSet.TimeDurationType; /** * Control Set for managing repeats @@ -31,11 +30,11 @@ public class TimerControlSet implements TaskEditControlSet { LayoutInflater.from(activity).inflate(R.layout.timer_control, parent, true); estimated = new TimeDurationTaskEditControlSet(Task.ESTIMATED_SECONDS, - R.id.estimatedDuration, 0, R.string.DLG_hour_minutes, - TimeDurationType.HOURS_MINUTES); + R.id.estimatedDuration, 0, R.string.DLG_hour_minutes + ); elapsed = new TimeDurationTaskEditControlSet(Task.ELAPSED_SECONDS, R.id.elapsedDuration, - 0, R.string.DLG_hour_minutes, - TimeDurationType.HOURS_MINUTES); + 0, R.string.DLG_hour_minutes + ); } @Override @@ -62,10 +61,10 @@ public class TimerControlSet implements TaskEditControlSet { private final IntegerProperty property; public TimeDurationTaskEditControlSet(IntegerProperty property, int timeButtonId, - int prefixResource, int titleResource, TimeDurationType type) { + int prefixResource, int titleResource) { this.property = property; this.controlSet = new TimeDurationControlSet(activity, - timeButtonId, prefixResource, titleResource, type); + timeButtonId, prefixResource, titleResource); } @Override diff --git a/astrid/res/drawable/ic_menu_refresh.png b/astrid/res/drawable/ic_menu_refresh.png new file mode 100644 index 000000000..0d706392c Binary files /dev/null and b/astrid/res/drawable/ic_menu_refresh.png differ diff --git a/astrid/res/layout/alarm_control.xml b/astrid/res/layout/alarm_control.xml new file mode 100644 index 000000000..ba11307fc --- /dev/null +++ b/astrid/res/layout/alarm_control.xml @@ -0,0 +1,23 @@ + + + + + + + + + +