diff --git a/trunk/.classpath b/trunk/.classpath
new file mode 100644
index 000000000..f4b028622
--- /dev/null
+++ b/trunk/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/trunk/.project b/trunk/.project
new file mode 100644
index 000000000..9fb6486f8
--- /dev/null
+++ b/trunk/.project
@@ -0,0 +1,33 @@
+
+
+ astrid
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/trunk/.settings/org.eclipse.jdt.core.prefs b/trunk/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..5c45a5c53
--- /dev/null
+++ b/trunk/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,60 @@
+#Wed Dec 17 03:02:43 PST 2008
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=error
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
diff --git a/trunk/.settings/org.eclipse.jdt.ui.prefs b/trunk/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 000000000..c994a0992
--- /dev/null
+++ b/trunk/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,4 @@
+#Mon Dec 22 14:49:19 PST 2008
+eclipse.preferences.version=1
+org.eclipse.jdt.ui.javadoc=false
+org.eclipse.jdt.ui.text.custom_code_templates=/**\n * @return the ${bare_field_name}\n *//**\n * @param ${param} the ${bare_field_name} to set\n *//**\n * ${tags}\n *//*\n * ASTRID\: Android's Simple Task Recording Dame\n *\n * This program is free software; you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation; either version 2 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License\n * for more details.\n *\n * You should have received a copy of the GNU General Public License along\n * with this program; if not, write to the Free Software Foundation, Inc.,\n * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n *//**\n * @author ${user}\n *\n * ${tags}\n *//**\n * \n *//**\n * ${tags}\n *//* (non-Javadoc)\n * ${see_to_overridden}\n *//**\n * ${tags}\n * ${see_to_target}\n */${filecomment}\n${package_declaration}\n\n${typecomment}\n${type_declaration}\n\n\n\nLog.e("", ${exception_var}.toString());${body_statement}${body_statement}\n// ${todo} Auto-generated constructor stubreturn ${field};${field} \= ${param};
diff --git a/trunk/AndroidManifest.xml b/trunk/AndroidManifest.xml
new file mode 100644
index 000000000..f34c60e7a
--- /dev/null
+++ b/trunk/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/trunk/lib/android-src.jar b/trunk/lib/android-src.jar
new file mode 100644
index 000000000..72bdaa151
Binary files /dev/null and b/trunk/lib/android-src.jar differ
diff --git a/trunk/lib/android.jar b/trunk/lib/android.jar
new file mode 100644
index 000000000..b26d3611b
Binary files /dev/null and b/trunk/lib/android.jar differ
diff --git a/trunk/res/drawable/highlight_pressed.9.png b/trunk/res/drawable/highlight_pressed.9.png
new file mode 100644
index 000000000..9bd2b50c0
Binary files /dev/null and b/trunk/res/drawable/highlight_pressed.9.png differ
diff --git a/trunk/res/drawable/highlight_selected.9.png b/trunk/res/drawable/highlight_selected.9.png
new file mode 100644
index 000000000..ecf0cada9
Binary files /dev/null and b/trunk/res/drawable/highlight_selected.9.png differ
diff --git a/trunk/res/drawable/ic_dialog_time.png b/trunk/res/drawable/ic_dialog_time.png
new file mode 100755
index 000000000..dffec296d
Binary files /dev/null and b/trunk/res/drawable/ic_dialog_time.png differ
diff --git a/trunk/res/drawable/icon.png b/trunk/res/drawable/icon.png
new file mode 100644
index 000000000..07acb847b
Binary files /dev/null and b/trunk/res/drawable/icon.png differ
diff --git a/trunk/res/drawable/strikeout.png b/trunk/res/drawable/strikeout.png
new file mode 100644
index 000000000..64e0c9394
Binary files /dev/null and b/trunk/res/drawable/strikeout.png differ
diff --git a/trunk/res/drawable/timepicker_down_btn.xml b/trunk/res/drawable/timepicker_down_btn.xml
new file mode 100644
index 000000000..61a252a88
--- /dev/null
+++ b/trunk/res/drawable/timepicker_down_btn.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trunk/res/drawable/timepicker_down_disabled.9.png b/trunk/res/drawable/timepicker_down_disabled.9.png
new file mode 100755
index 000000000..af72d2299
Binary files /dev/null and b/trunk/res/drawable/timepicker_down_disabled.9.png differ
diff --git a/trunk/res/drawable/timepicker_down_disabled_focused.9.png b/trunk/res/drawable/timepicker_down_disabled_focused.9.png
new file mode 100755
index 000000000..2d80424af
Binary files /dev/null and b/trunk/res/drawable/timepicker_down_disabled_focused.9.png differ
diff --git a/trunk/res/drawable/timepicker_down_normal.9.png b/trunk/res/drawable/timepicker_down_normal.9.png
new file mode 100755
index 000000000..c427fc348
Binary files /dev/null and b/trunk/res/drawable/timepicker_down_normal.9.png differ
diff --git a/trunk/res/drawable/timepicker_down_pressed.9.png b/trunk/res/drawable/timepicker_down_pressed.9.png
new file mode 100755
index 000000000..ac6ac5338
Binary files /dev/null and b/trunk/res/drawable/timepicker_down_pressed.9.png differ
diff --git a/trunk/res/drawable/timepicker_down_selected.9.png b/trunk/res/drawable/timepicker_down_selected.9.png
new file mode 100755
index 000000000..f710b5796
Binary files /dev/null and b/trunk/res/drawable/timepicker_down_selected.9.png differ
diff --git a/trunk/res/drawable/timepicker_input.xml b/trunk/res/drawable/timepicker_input.xml
new file mode 100644
index 000000000..b811d4e3a
--- /dev/null
+++ b/trunk/res/drawable/timepicker_input.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trunk/res/drawable/timepicker_input_disabled.9.png b/trunk/res/drawable/timepicker_input_disabled.9.png
new file mode 100755
index 000000000..97da87a8d
Binary files /dev/null and b/trunk/res/drawable/timepicker_input_disabled.9.png differ
diff --git a/trunk/res/drawable/timepicker_input_normal.9.png b/trunk/res/drawable/timepicker_input_normal.9.png
new file mode 100755
index 000000000..eb101c58e
Binary files /dev/null and b/trunk/res/drawable/timepicker_input_normal.9.png differ
diff --git a/trunk/res/drawable/timepicker_input_pressed.9.png b/trunk/res/drawable/timepicker_input_pressed.9.png
new file mode 100755
index 000000000..c83b1eb4a
Binary files /dev/null and b/trunk/res/drawable/timepicker_input_pressed.9.png differ
diff --git a/trunk/res/drawable/timepicker_input_selected.9.png b/trunk/res/drawable/timepicker_input_selected.9.png
new file mode 100755
index 000000000..e15284895
Binary files /dev/null and b/trunk/res/drawable/timepicker_input_selected.9.png differ
diff --git a/trunk/res/drawable/timepicker_up_btn.xml b/trunk/res/drawable/timepicker_up_btn.xml
new file mode 100644
index 000000000..5428aeeb6
--- /dev/null
+++ b/trunk/res/drawable/timepicker_up_btn.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trunk/res/drawable/timepicker_up_disabled.9.png b/trunk/res/drawable/timepicker_up_disabled.9.png
new file mode 100755
index 000000000..1814bb4db
Binary files /dev/null and b/trunk/res/drawable/timepicker_up_disabled.9.png differ
diff --git a/trunk/res/drawable/timepicker_up_disabled_focused.9.png b/trunk/res/drawable/timepicker_up_disabled_focused.9.png
new file mode 100755
index 000000000..9ad5b85c9
Binary files /dev/null and b/trunk/res/drawable/timepicker_up_disabled_focused.9.png differ
diff --git a/trunk/res/drawable/timepicker_up_normal.9.png b/trunk/res/drawable/timepicker_up_normal.9.png
new file mode 100755
index 000000000..35fc221c1
Binary files /dev/null and b/trunk/res/drawable/timepicker_up_normal.9.png differ
diff --git a/trunk/res/drawable/timepicker_up_pressed.9.png b/trunk/res/drawable/timepicker_up_pressed.9.png
new file mode 100755
index 000000000..c910777d4
Binary files /dev/null and b/trunk/res/drawable/timepicker_up_pressed.9.png differ
diff --git a/trunk/res/drawable/timepicker_up_selected.9.png b/trunk/res/drawable/timepicker_up_selected.9.png
new file mode 100755
index 000000000..549a7e575
Binary files /dev/null and b/trunk/res/drawable/timepicker_up_selected.9.png differ
diff --git a/trunk/res/drawable/transparent_button.xml b/trunk/res/drawable/transparent_button.xml
new file mode 100644
index 000000000..42c7355c8
--- /dev/null
+++ b/trunk/res/drawable/transparent_button.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/trunk/res/layout/number_picker.xml b/trunk/res/layout/number_picker.xml
new file mode 100644
index 000000000..f3d6d8366
--- /dev/null
+++ b/trunk/res/layout/number_picker.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/trunk/res/layout/number_picker_dialog.xml b/trunk/res/layout/number_picker_dialog.xml
new file mode 100644
index 000000000..b59db5840
--- /dev/null
+++ b/trunk/res/layout/number_picker_dialog.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/trunk/res/layout/task_edit.xml b/trunk/res/layout/task_edit.xml
new file mode 100644
index 000000000..218367ef4
--- /dev/null
+++ b/trunk/res/layout/task_edit.xml
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trunk/res/layout/task_list.xml b/trunk/res/layout/task_list.xml
new file mode 100644
index 000000000..877b46cd6
--- /dev/null
+++ b/trunk/res/layout/task_list.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/trunk/res/layout/task_list_row.xml b/trunk/res/layout/task_list_row.xml
new file mode 100644
index 000000000..750581779
--- /dev/null
+++ b/trunk/res/layout/task_list_row.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/trunk/res/layout/task_view.xml b/trunk/res/layout/task_view.xml
new file mode 100644
index 000000000..2cedf32b4
--- /dev/null
+++ b/trunk/res/layout/task_view.xml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/trunk/res/values/arrays.xml b/trunk/res/values/arrays.xml
new file mode 100644
index 000000000..ee8cf9928
--- /dev/null
+++ b/trunk/res/values/arrays.xml
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+ - 5 minutes
+ - 10 minutes
+ - 15 minutes
+ - 20 minutes
+ - 25 minutes
+ - 30 minutes
+ - 45 minutes
+ - 1 hour
+ - 2 hours
+ - 3 hours
+ - 12 hours
+ - 24 hours
+ - 2 days
+ - 1 week
+
+
+
+ - "5"
+ - "10"
+ - "15"
+ - "20"
+ - "25"
+ - "30"
+ - "45"
+ - "60"
+ - "120"
+ - "180"
+ - "720"
+ - "1440"
+ - "2880"
+ - "10080"
+
+
+
+ - None
+ - 5 minutes
+ - 10 minutes
+ - 15 minutes
+ - 20 minutes
+ - 25 minutes
+ - 30 minutes
+ - 45 minutes
+ - 1 hour
+ - 2 hours
+ - 3 hours
+ - 12 hours
+ - 24 hours
+ - 2 days
+ - 1 week
+
+
+
+ - "0"
+ - "5"
+ - "10"
+ - "15"
+ - "20"
+ - "25"
+ - "30"
+ - "45"
+ - "60"
+ - "120"
+ - "180"
+ - "720"
+ - "1440"
+ - "2880"
+ - "10080"
+
+
+
+ - Alert
+ - Status bar notification
+ - Off
+
+
+
+ - "0"
+ - "1"
+ - "2"
+
+
+
+ - Busy
+ - Available
+
+
+
+ - Default
+ - Private
+ - Public
+
+
+
+
+ - Sunday
+ - Monday
+ - Tuesday
+ - Wednesday
+ - Thursday
+ - Friday
+ - Saturday
+
+
+
+ - first
+ - second
+ - third
+ - fourth
+ - last
+
+
+
+
+ - (No response)
+ - Yes
+ - Maybe
+ - No
+
+
+ - Yes
+ - Maybe
+ - No
+
+
+
+
+ - Only this event
+ - This & future events
+ - All events
+
+
+
+
+
+ - This & future events
+ - All events
+
+
diff --git a/trunk/res/values/colors.xml b/trunk/res/values/colors.xml
new file mode 100644
index 000000000..ca2c1d6c4
--- /dev/null
+++ b/trunk/res/values/colors.xml
@@ -0,0 +1,33 @@
+
+
+
+ #ffff4444
+ #ffaaaaaa
+
+ #ff83ffa9
+ #ffbbbbbb
+ #ffff0000
+
+ #ffe36150
+ #ffe3ad50
+ #ffffffff
+ #ffb5b0a8
+
+
diff --git a/trunk/res/values/strings.xml b/trunk/res/values/strings.xml
new file mode 100644
index 000000000..5de126290
--- /dev/null
+++ b/trunk/res/values/strings.xml
@@ -0,0 +1,103 @@
+
+
+
+ Astrid
+
+
+
+
+
+ I need to do this
+ I really should do this
+ It would be nice to do this
+ Who cares!
+
+
+
+ - 1 Task
+ - %d Tasks
+
+
+
+ - 1 Day
+ - %d Days
+
+
+ - 1 Hour
+ - %d Hours
+
+
+ - 1 Minute
+ - %d Minutes
+
+
+ - 1 Second
+ - %d Seconds
+
+
+
+
+ Astrid:
+ hidden
+ New Task
+
+ Add
+ Tags
+ Settings
+ Filters
+
+ Edit Task
+ Delete Task
+ Filters
+ Hidden/Blocked Tasks
+ Completed Tasks
+
+
+
+
+ Astrid: Editing Task
+ Astrid: Editing
+
+ What
+ Task Description
+ Estimated Time Needed
+ Time Already Spent on Task
+ How important is it?
+ Tags
+ Absolute Deadline
+ Goal Deadline
+ Hide Until This Date
+ Blocking On
+ Notes
+ Enter Task Notes
+ Time (in minutes)
+
+ Save
+ Discard
+ Delete
+ Click to Set
+
+ Save
+
+
+
+
+ Astrid: Task Properties
+ Start Timer
+ Stop Timer
+ % Done
+ Edit Task
+ Elapsed Time
+ Estimated Time
+ Absolute Deadline
+ Goal Deadline
+ Task Notes
+ Overdue
+ % of Task Finished
+
+
+
+ Delete
+ Delete this task?
+
+
diff --git a/trunk/res/values/styles.xml b/trunk/res/values/styles.xml
new file mode 100644
index 000000000..64dfb61e5
--- /dev/null
+++ b/trunk/res/values/styles.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trunk/src/com/timsu/astrid/R.java b/trunk/src/com/timsu/astrid/R.java
new file mode 100644
index 000000000..ed49aba03
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/R.java
@@ -0,0 +1,223 @@
+/* AUTO-GENERATED FILE. DO NOT MODIFY.
+ *
+ * This class was automatically generated by the
+ * aapt tool from the resource data it found. It
+ * should not be modified by hand.
+ */
+
+package com.timsu.astrid;
+
+public final class R {
+ public static final class array {
+ public static final int availability=0x7f040006;
+ /** Order matters, and note that the preference for which day the week starts on is handled
+ elsewhere (and needn't be addressed here).
+ */
+ public static final int day_labels=0x7f040008;
+ /** The corresponding indices are defined in DeleteEventHelper.java
+ */
+ public static final int delete_repeating_labels=0x7f04000c;
+ /** The corresponding indices are defined in DeleteEventHelper.java
+ This is the same array as above (the "delete_repeating_labels" array,
+ except that the first element "Only this event" is removed. This
+ array exists to work-around a bug in the CalendarProvider and sync
+ code where you can't delete one instance of a repeating event that
+ was created on the phone until that event has been synced to the server.
+
+ */
+ public static final int delete_repeating_labels_no_selected=0x7f04000d;
+ public static final int ordinal_labels=0x7f040009;
+ public static final int preferences_alert_type_labels=0x7f040004;
+ public static final int preferences_alert_type_values=0x7f040005;
+ public static final int preferences_default_reminder_labels=0x7f040002;
+ public static final int preferences_default_reminder_values=0x7f040003;
+ /** Choices for the "Reminder minutes" spinner.
+ These must be kept in sync with the reminder_minutes_values array.
+
+ */
+ public static final int reminder_minutes_labels=0x7f040000;
+ public static final int reminder_minutes_values=0x7f040001;
+ /** Invitation responses
+ */
+ public static final int response_labels1=0x7f04000a;
+ public static final int response_labels2=0x7f04000b;
+ public static final int visibility=0x7f040007;
+ }
+ public static final class attr {
+ }
+ public static final class color {
+ public static final int importance_1=0x7f050005;
+ public static final int importance_2=0x7f050006;
+ public static final int importance_3=0x7f050007;
+ public static final int importance_4=0x7f050008;
+ public static final int task_list_done=0x7f050001;
+ public static final int task_list_overdue=0x7f050000;
+ public static final int view_header_done=0x7f050002;
+ public static final int view_table_overdue=0x7f050004;
+ public static final int view_table_values=0x7f050003;
+ }
+ public static final class drawable {
+ public static final int highlight_pressed=0x7f020000;
+ public static final int highlight_selected=0x7f020001;
+ public static final int ic_dialog_time=0x7f020002;
+ public static final int icon=0x7f020003;
+ public static final int strikeout=0x7f020004;
+ public static final int timepicker_down_btn=0x7f020005;
+ public static final int timepicker_down_disabled=0x7f020006;
+ public static final int timepicker_down_disabled_focused=0x7f020007;
+ public static final int timepicker_down_normal=0x7f020008;
+ public static final int timepicker_down_pressed=0x7f020009;
+ public static final int timepicker_down_selected=0x7f02000a;
+ public static final int timepicker_input=0x7f02000b;
+ public static final int timepicker_input_disabled=0x7f02000c;
+ public static final int timepicker_input_normal=0x7f02000d;
+ public static final int timepicker_input_pressed=0x7f02000e;
+ public static final int timepicker_input_selected=0x7f02000f;
+ public static final int timepicker_up_btn=0x7f020010;
+ public static final int timepicker_up_disabled=0x7f020011;
+ public static final int timepicker_up_disabled_focused=0x7f020012;
+ public static final int timepicker_up_normal=0x7f020013;
+ public static final int timepicker_up_pressed=0x7f020014;
+ public static final int timepicker_up_selected=0x7f020015;
+ public static final int transparent_button=0x7f020016;
+ }
+ public static final class id {
+ public static final int addtask=0x7f090023;
+ public static final int button_layout=0x7f090029;
+ public static final int cb1=0x7f090025;
+ public static final int cell_definiteDueDate=0x7f09002f;
+ public static final int cell_elapsed=0x7f09002d;
+ public static final int cell_estimated=0x7f09002e;
+ public static final int cell_notes=0x7f090031;
+ public static final int cell_preferredDueDate=0x7f090030;
+ public static final int dates_container=0x7f09000d;
+ public static final int decrement=0x7f090002;
+ public static final int definiteDueDate_date=0x7f090010;
+ public static final int definiteDueDate_label=0x7f09000e;
+ public static final int definiteDueDate_notnull=0x7f09000f;
+ public static final int definiteDueDate_time=0x7f090011;
+ public static final int delete=0x7f090020;
+ public static final int discard=0x7f09001f;
+ public static final int edit=0x7f09002c;
+ public static final int elapsedDuration=0x7f09001d;
+ public static final int elapsedDuration_label=0x7f09001c;
+ public static final int estimatedDuration=0x7f09000a;
+ public static final int estimatedDuration_label=0x7f090009;
+ public static final int event=0x7f090005;
+ public static final int hiddenUntil_date=0x7f090018;
+ public static final int hiddenUntil_label=0x7f090016;
+ public static final int hiddenUntil_notnull=0x7f090017;
+ public static final int hiddenUntil_time=0x7f090019;
+ public static final int image1=0x7f090026;
+ public static final int importance=0x7f09000c;
+ public static final int importance_label=0x7f09000b;
+ public static final int increment=0x7f090000;
+ public static final int name=0x7f090007;
+ public static final int name_label=0x7f090006;
+ public static final int notes=0x7f09001b;
+ public static final int notes_label=0x7f09001a;
+ public static final int numberPicker=0x7f090003;
+ public static final int preferredDueDate_date=0x7f090014;
+ public static final int preferredDueDate_label=0x7f090012;
+ public static final int preferredDueDate_notnull=0x7f090013;
+ public static final int preferredDueDate_time=0x7f090015;
+ public static final int progress=0x7f09002b;
+ public static final int properties_container=0x7f090008;
+ public static final int row_layout=0x7f090024;
+ public static final int save=0x7f09001e;
+ public static final int scroll_view=0x7f090004;
+ public static final int tasklist=0x7f090022;
+ public static final int tasklist_layout=0x7f090021;
+ public static final int text1=0x7f090027;
+ public static final int timepicker_input=0x7f090001;
+ public static final int timerButton=0x7f09002a;
+ public static final int view_layout=0x7f090028;
+ }
+ public static final class layout {
+ public static final int number_picker=0x7f030000;
+ public static final int number_picker_dialog=0x7f030001;
+ public static final int task_edit=0x7f030002;
+ public static final int task_list=0x7f030003;
+ public static final int task_list_row=0x7f030004;
+ public static final int task_view=0x7f030005;
+ }
+ public static final class plurals {
+ /** Time Constants
+ */
+ public static final int Ndays=0x7f070001;
+ public static final int Nhours=0x7f070002;
+ public static final int Nminutes=0x7f070003;
+ public static final int Nseconds=0x7f070004;
+ /** Plurals
+ */
+ public static final int Ntasks=0x7f070000;
+ }
+ public static final class string {
+ public static final int addtask_label=0x7f060007;
+ /** application
+ */
+ public static final int app_name=0x7f060000;
+ public static final int blank_button_title=0x7f060023;
+ public static final int blockingOn_label=0x7f06001c;
+ public static final int definiteDueDate_label=0x7f060019;
+ public static final int delete_label=0x7f060022;
+ public static final int delete_this_task_title=0x7f060032;
+ public static final int delete_title=0x7f060031;
+ public static final int discard_label=0x7f060021;
+ public static final int edit_label=0x7f060029;
+ public static final int elapsedDuration_label=0x7f060016;
+ public static final int estimatedDuration_label=0x7f060015;
+ public static final int hiddenUntil_label=0x7f06001b;
+ /** Importance Labels
+ */
+ public static final int importance_1=0x7f060001;
+ public static final int importance_2=0x7f060002;
+ public static final int importance_3=0x7f060003;
+ public static final int importance_4=0x7f060004;
+ public static final int importance_label=0x7f060017;
+ public static final int minutes_dialog=0x7f06001f;
+ public static final int name_hint=0x7f060014;
+ public static final int name_label=0x7f060013;
+ public static final int notes_hint=0x7f06001e;
+ public static final int notes_label=0x7f06001d;
+ public static final int overdue_suffix=0x7f06002f;
+ public static final int preferredDueDate_label=0x7f06001a;
+ public static final int progress_dialog=0x7f060030;
+ public static final int progress_suffix=0x7f060028;
+ public static final int save_label=0x7f060020;
+ public static final int startTimer_label=0x7f060026;
+ public static final int stopTimer_label=0x7f060027;
+ public static final int tags_label=0x7f060018;
+ public static final int taskEdit_menu_save=0x7f060024;
+ public static final int taskEdit_titleGeneric=0x7f060011;
+ public static final int taskEdit_titlePrefix=0x7f060012;
+ public static final int taskList_context_delete=0x7f06000d;
+ public static final int taskList_context_edit=0x7f06000c;
+ public static final int taskList_filter_done=0x7f060010;
+ public static final int taskList_filter_hidden=0x7f06000f;
+ public static final int taskList_filter_title=0x7f06000e;
+ public static final int taskList_hiddenSuffix=0x7f060006;
+ public static final int taskList_menu_filters=0x7f06000b;
+ public static final int taskList_menu_insert=0x7f060008;
+ public static final int taskList_menu_settings=0x7f06000a;
+ public static final int taskList_menu_tags=0x7f060009;
+ public static final int taskList_titlePrefix=0x7f060005;
+ public static final int taskView_definiteDueDate=0x7f06002c;
+ public static final int taskView_elapsed=0x7f06002a;
+ public static final int taskView_estimated=0x7f06002b;
+ public static final int taskView_notes=0x7f06002e;
+ public static final int taskView_preferredDueDate=0x7f06002d;
+ public static final int taskView_title=0x7f060025;
+ }
+ public static final class style {
+ public static final int Alert=0x7f080000;
+ public static final int MonthView_DayLabel=0x7f080001;
+ public static final int TextAppearance=0x7f080002;
+ public static final int TextAppearance_AgendaView_ValueLabel=0x7f080004;
+ public static final int TextAppearance_Alert_Label=0x7f080007;
+ public static final int TextAppearance_Alert_Title=0x7f080006;
+ public static final int TextAppearance_Alert_Value=0x7f080008;
+ public static final int TextAppearance_EditEvent_Label=0x7f080005;
+ public static final int TextAppearance_MonthView_DayLabel=0x7f080003;
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/activities/TaskEdit.java b/trunk/src/com/timsu/astrid/activities/TaskEdit.java
new file mode 100644
index 000000000..e3649097d
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/activities/TaskEdit.java
@@ -0,0 +1,459 @@
+/*
+ * ASTRID: Android's Simple Task Recording Dame
+ *
+ * Copyright (c) 2009 Tim Su
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.timsu.astrid.activities;
+
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import android.app.AlertDialog;
+import android.app.DatePickerDialog;
+import android.app.TimePickerDialog;
+import android.app.DatePickerDialog.OnDateSetListener;
+import android.app.TimePickerDialog.OnTimeSetListener;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TimePicker;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+import com.timsu.astrid.R;
+import com.timsu.astrid.data.enums.Importance;
+import com.timsu.astrid.data.task.TaskIdentifier;
+import com.timsu.astrid.data.task.TaskModelForEdit;
+import com.timsu.astrid.utilities.DateUtilities;
+import com.timsu.astrid.widget.NumberPicker;
+import com.timsu.astrid.widget.NumberPickerDialog;
+import com.timsu.astrid.widget.NumberPickerDialog.OnNumberPickedListener;
+
+public class TaskEdit extends TaskModificationActivity {
+ private static final int SAVE_ID = Menu.FIRST;
+ private static final int DISCARD_ID = Menu.FIRST + 1;
+ private static final int DELETE_ID = Menu.FIRST + 2;
+
+ private EditText name;
+ private Spinner importance;
+ private TimeDurationControlSet estimatedDuration;
+ private TimeDurationControlSet elapsedDuration;
+ private DateControlSet definiteDueDate;
+ private DateControlSet preferredDueDate;
+ private DateControlSet hiddenUntil;
+ private EditText notes;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.task_edit);
+
+ setUpUIComponents();
+ setUpListeners();
+ populateFields();
+ }
+
+ @Override
+ protected TaskModelForEdit getModel(TaskIdentifier identifier) {
+ if (identifier != null)
+ return controller.fetchTaskForEdit(identifier);
+ else
+ return controller.createNewTaskForEdit();
+ }
+
+ // --- data saving and retrieving
+
+ private void populateFields() {
+ Resources r = getResources();
+ if(model.getCursor() != null)
+ startManagingCursor(model.getCursor());
+
+ name.setText(model.getName());
+ if(model.getName().length() > 0)
+ setTitle(new StringBuilder().
+ append(r.getString(R.string.taskEdit_titlePrefix)).
+ append(" ").
+ append(model.getName()));
+
+ estimatedDuration.setTimeElapsed(model.getEstimatedSeconds());
+ elapsedDuration.setTimeElapsed(model.getElapsedSeconds());
+ importance.setSelection(model.getImportance().ordinal());
+
+ definiteDueDate.setDate(model.getDefiniteDueDate());
+ preferredDueDate.setDate(model.getPreferredDueDate());
+ hiddenUntil.setDate(model.getHiddenUntil());
+
+ notes.setText(model.getNotes());
+ }
+
+ private void save() {
+ model.setName(name.getText().toString());
+ model.setEstimatedSeconds(estimatedDuration.getTimeDurationInSeconds());
+ model.setElapsedSeconds(elapsedDuration.getTimeDurationInSeconds());
+ model.setImportance(Importance.values()[importance.getSelectedItemPosition()]);
+
+ model.setDefiniteDueDate(definiteDueDate.getDate());
+ model.setPreferredDueDate(preferredDueDate.getDate());
+ model.setHiddenUntil(hiddenUntil.getDate());
+
+ model.setNotes(notes.getText().toString());
+
+ try {
+ if(!controller.saveTask(model))
+ throw new RuntimeException("Unable to save task: false");
+ } catch (RuntimeException e) {
+ Log.e(getClass().getSimpleName(), "Error saving task!", e);
+ }
+ }
+
+ // --- user interface components
+
+ private void setUpUIComponents() {
+ Resources r = getResources();
+ setTitle(new StringBuilder()
+ .append(r.getString(R.string.app_name))
+ .append(": ")
+ .append(r.getString(R.string.taskEdit_titleGeneric)));
+
+ name = (EditText)findViewById(R.id.name);
+ importance = (Spinner)findViewById(R.id.importance);
+
+ estimatedDuration = new TimeDurationControlSet(R.id.estimatedDuration);
+ elapsedDuration = new TimeDurationControlSet(R.id.elapsedDuration);
+ definiteDueDate = new DateControlSet(R.id.definiteDueDate_notnull,
+ R.id.definiteDueDate_date, R.id.definiteDueDate_time);
+ preferredDueDate = new DateControlSet(R.id.preferredDueDate_notnull,
+ R.id.preferredDueDate_date, R.id.preferredDueDate_time);
+ hiddenUntil = new DateControlSet(R.id.hiddenUntil_notnull,
+ R.id.hiddenUntil_date, R.id.hiddenUntil_time);
+
+ notes = (EditText)findViewById(R.id.notes);
+
+ // set up for each field
+
+ ImportanceAdapter importanceAdapter = new ImportanceAdapter(this,
+ android.R.layout.simple_spinner_item,
+ android.R.layout.simple_spinner_dropdown_item,
+ Importance.values());
+ importance.setAdapter(importanceAdapter);
+ }
+
+ /** Display importance with proper formatting */
+ private class ImportanceAdapter extends ArrayAdapter {
+ private int textViewResourceId, dropDownResourceId;
+ private LayoutInflater inflater;
+
+ public ImportanceAdapter(Context context, int textViewResourceId,
+ int dropDownResourceId, Importance[] objects) {
+ super(context, textViewResourceId, objects);
+
+ inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ this.textViewResourceId = textViewResourceId;
+ this.dropDownResourceId = dropDownResourceId;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return getView(position, convertView, parent, textViewResourceId, true);
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return getView(position, convertView, parent, dropDownResourceId, true);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent,
+ int resource, boolean setColors) {
+ View view;
+ TextView text;
+ Resources r = getResources();
+
+ if (convertView == null) {
+ view = inflater.inflate(resource, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ try {
+ text = (TextView) view;
+ } catch (ClassCastException e) {
+ Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
+ throw new IllegalStateException(
+ "ArrayAdapter requires the resource ID to be a TextView", e);
+ }
+
+ text.setText(r.getString(getItem(position).getLabelResource()));
+ if(setColors)
+ text.setBackgroundColor(r.getColor(getItem(position).getColorResource()));
+
+ return view;
+ }
+ }
+
+ /** Set up button listeners */
+ private void setUpListeners() {
+
+ Button saveButton = (Button) findViewById(R.id.save);
+ saveButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View view) {
+ saveButtonClick();
+ }
+ });
+
+ Button discardButton = (Button) findViewById(R.id.discard);
+ discardButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View view) {
+ discardButtonClick();
+ }
+ });
+
+ Button deleteButton = (Button) findViewById(R.id.delete);
+ if(model.getTaskIdentifier() == null)
+ deleteButton.setVisibility(View.GONE);
+ deleteButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View view) {
+ deleteButtonClick();
+ }
+ });
+ }
+
+ private void saveButtonClick() {
+ save();
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ private void discardButtonClick() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ private void deleteButtonClick() {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.delete_title)
+ .setMessage(R.string.delete_this_task_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ controller.deleteTask(model.getTaskIdentifier());
+ setResult(RESULT_OK);
+ finish();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ switch(item.getItemId()) {
+ case SAVE_ID:
+ saveButtonClick();
+ return true;
+ case DISCARD_ID:
+ discardButtonClick();
+ return true;
+ case DELETE_ID:
+ deleteButtonClick();
+ return true;
+ }
+
+ return super.onMenuItemSelected(featureId, item);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuItem item;
+
+ item = menu.add(Menu.NONE, SAVE_ID, 0, R.string.save_label);
+ item.setIcon(android.R.drawable.ic_menu_save);
+ item.setAlphabeticShortcut('s');
+
+ item = menu.add(Menu.NONE, DISCARD_ID, 0, R.string.discard_label);
+ item.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ item.setAlphabeticShortcut('c');
+
+ item = menu.add(Menu.NONE, DISCARD_ID, 0, R.string.delete_label);
+ item.setIcon(android.R.drawable.ic_menu_delete);
+ item.setAlphabeticShortcut('d');
+
+ return true;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ save();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ populateFields();
+ }
+
+ // --- date/time methods and helper classes
+
+ private class TimeDurationControlSet implements OnNumberPickedListener,
+ View.OnClickListener {
+ private Button timeButton;
+ private int timeDuration;
+ private final NumberPickerDialog dialog =
+ new NumberPickerDialog(TaskEdit.this, this,
+ getResources().getString(R.string.minutes_dialog),
+ 0, 5, 0, 999);
+
+ public TimeDurationControlSet(int timeButtonId) {
+ timeButton = (Button)findViewById(timeButtonId);
+ timeButton.setOnClickListener(this);
+ }
+
+ public int getTimeDurationInSeconds() {
+ return timeDuration;
+ }
+
+ public void setTimeElapsed(Integer timeDurationInSeconds) {
+ if(timeDurationInSeconds == null)
+ timeDurationInSeconds = 0;
+
+ timeDuration = timeDurationInSeconds;
+
+ Resources r = getResources();
+ if(timeDurationInSeconds == 0) {
+ timeButton.setText(r.getString(R.string.blank_button_title));
+ return;
+ }
+
+ timeButton.setText(DateUtilities.getDurationString(r,
+ timeDurationInSeconds, 2));
+ dialog.setInitialValue(timeDuration/60);
+ }
+
+ @Override
+ /** Called when NumberPicker activity is completed */
+ public void onNumberPicked(NumberPicker view, int value) {
+ setTimeElapsed(value * 60);
+ }
+
+ /** Called when time button is clicked */
+ public void onClick(View v) {
+ dialog.show();
+ }
+
+
+ }
+
+ private static final Format dateFormatter = new SimpleDateFormat("EEE, MMM d, yyyy");
+ private static final Format timeFormatter = new SimpleDateFormat("h:mm a");
+
+ private class DateControlSet implements OnTimeSetListener,
+ OnDateSetListener, View.OnClickListener {
+ private CheckBox activatedCheckBox;
+ private Button dateButton;
+ private Button timeButton;
+ private Date date;
+
+ public DateControlSet(int checkBoxId, int dateButtonId, int timeButtonId) {
+ activatedCheckBox = (CheckBox)findViewById(checkBoxId);
+ dateButton = (Button)findViewById(dateButtonId);
+ timeButton = (Button)findViewById(timeButtonId);
+
+ activatedCheckBox.setOnCheckedChangeListener(
+ new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ dateButton.setEnabled(isChecked);
+ timeButton.setEnabled(isChecked);
+ }
+ });
+ dateButton.setOnClickListener(this);
+ timeButton.setOnClickListener(this);
+ }
+
+ public Date getDate() {
+ if(!activatedCheckBox.isChecked())
+ return null;
+ return date;
+ }
+
+ /** Initialize the components for the given date field */
+ public void setDate(Date newDate) {
+ this.date = newDate;
+ if(newDate == null) {
+ date = new Date();
+ date.setMinutes(0);
+ }
+
+ activatedCheckBox.setChecked(newDate != null);
+ dateButton.setEnabled(newDate != null);
+ timeButton.setEnabled(newDate != null);
+
+ updateDate();
+ updateTime();
+ }
+
+ public void onDateSet(DatePicker view, int year, int month, int monthDay) {
+ date.setYear(year - 1900);
+ date.setMonth(month);
+ date.setDate(monthDay);
+ updateDate();
+ }
+
+ public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
+ date.setHours(hourOfDay);
+ date.setMinutes(minute);
+ updateTime();
+ }
+
+ public void updateDate() {
+ dateButton.setText(dateFormatter.format(date));
+
+ }
+
+ public void updateTime() {
+ timeButton.setText(timeFormatter.format(date));
+ }
+
+ public void onClick(View v) {
+ if(v == timeButton)
+ new TimePickerDialog(TaskEdit.this, this, date.getHours(),
+ date.getMinutes(), false).show();
+ else
+ new DatePickerDialog(TaskEdit.this, this, 1900 +
+ date.getYear(), date.getMonth(), date.getDate()).show();
+ }
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/activities/TaskList.java b/trunk/src/com/timsu/astrid/activities/TaskList.java
new file mode 100644
index 000000000..74e13442a
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/activities/TaskList.java
@@ -0,0 +1,401 @@
+/*
+ * ASTRID: Android's Simple Task Recording Dame
+ *
+ * Copyright (c) 2009 Tim Su
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.timsu.astrid.activities;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+import com.timsu.astrid.R;
+import com.timsu.astrid.data.task.TaskController;
+import com.timsu.astrid.data.task.TaskIdentifier;
+import com.timsu.astrid.data.task.TaskModelForList;
+
+
+/** Primary view for the Astrid Application. Lists all of the tasks in the
+ * system, and allows users to edit them.
+ *
+ * @author Tim Su (timsu@stanfordalumni.org)
+ *
+ */
+public class TaskList extends Activity {
+ private static final int ACTIVITY_CREATE = 0;
+ private static final int ACTIVITY_VIEW = 1;
+ private static final int ACTIVITY_EDIT = 2;
+
+ private static final int INSERT_ID = Menu.FIRST;
+ private static final int FILTERS_ID = Menu.FIRST + 1;
+ private static final int TAGS_ID = Menu.FIRST + 2;
+
+ private static final int CONTEXT_EDIT_ID = Menu.FIRST + 10;
+ private static final int CONTEXT_DELETE_ID = Menu.FIRST + 11;
+
+ private static final int CONTEXT_FILTER_HIDDEN = Menu.FIRST + 20;
+ private static final int CONTEXT_FILTER_DONE = Menu.FIRST + 21;
+
+ private TaskController controller;
+ private ListView listView;
+ private Button addButton;
+
+ private boolean filterShowHidden = false;
+ private boolean filterShowDone = false;
+
+ /** Called when loading up the activity for the first time */
+ private void onLoad() {
+ controller = new TaskController(this);
+ controller.open();
+
+ listView = (ListView)findViewById(R.id.tasklist);
+
+ addButton = (Button)findViewById(R.id.addtask);
+ addButton.setOnClickListener(new
+ View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ createTask();
+ }
+ });
+
+ fillData();
+
+ // filters context menu
+ listView.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ if(menu.hasVisibleItems())
+ return;
+
+ MenuItem item = menu.add(Menu.NONE, CONTEXT_FILTER_HIDDEN, Menu.NONE,
+ R.string.taskList_filter_hidden);
+ item.setCheckable(true);
+ item.setChecked(filterShowHidden);
+
+ item = menu.add(Menu.NONE, CONTEXT_FILTER_DONE, Menu.NONE,
+ R.string.taskList_filter_done);
+ item.setCheckable(true);
+ item.setChecked(filterShowDone);
+
+ menu.setHeaderTitle(R.string.taskList_filter_title);
+ }
+ });
+ }
+
+ /** Fill in the Task List with our tasks */
+ private void fillData() {
+ Resources r = getResources();
+
+ // get the database cursor
+ Cursor tasksCursor;
+ if(filterShowDone)
+ tasksCursor = controller.getAllTaskListCursor();
+ else
+ tasksCursor = controller.getActiveTaskListCursor();
+
+ startManagingCursor(tasksCursor);
+ int totalTasks = tasksCursor.getCount();
+ List taskArray =
+ controller.createTaskListFromCursor(tasksCursor, !filterShowHidden);
+ int hiddenTasks = totalTasks - taskArray.size();
+
+ // hide "add" button if we have a few tasks
+ if(taskArray.size() > 2)
+ addButton.setVisibility(View.GONE);
+
+ // set up the title
+ StringBuilder title = new StringBuilder().
+ append(r.getString(R.string.taskList_titlePrefix)).
+ append(" ").append(r.getQuantityString(R.plurals.Ntasks,
+ taskArray.size(), taskArray.size()));
+ if(hiddenTasks > 0)
+ title.append(" (").append(hiddenTasks).append(" ").
+ append(r.getString(R.string.taskList_hiddenSuffix)).append(")");
+ setTitle(title);
+
+ // set up our adapter
+ TaskListAdapter tasks = new TaskListAdapter(this,
+ R.layout.task_list_row, taskArray);
+ listView.setAdapter(tasks);
+ listView.setItemsCanFocus(true);
+
+ // list view listener
+ listView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view,
+ int position, long id) {
+ TaskModelForList task = (TaskModelForList)view.getTag();
+
+ Intent intent = new Intent(TaskList.this, TaskView.class);
+ intent.putExtra(TaskEdit.LOAD_INSTANCE_TOKEN, task.
+ getTaskIdentifier().getId());
+ startActivityForResult(intent, ACTIVITY_VIEW);
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent intent) {
+ fillData();
+ }
+
+ // --- list adapter
+
+ private class TaskListAdapter extends ArrayAdapter {
+
+ private List objects;
+ private int resource;
+ private LayoutInflater inflater;
+
+ public TaskListAdapter(Context context, int resource,
+ List objects) {
+ super(context, resource, objects);
+
+ inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ this.objects = objects;
+ this.resource = resource;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+
+ view = inflater.inflate(resource, parent, false);
+ setupView(view, objects.get(position));
+ addListeners(position, view);
+
+ return view;
+ }
+
+ public void setupView(View view, final TaskModelForList task) {
+ Resources r = getResources();
+
+ // set up basic properties
+ final TextView name = ((TextView)view.findViewById(R.id.text1));
+ final CheckBox progress = ((CheckBox)view.findViewById(R.id.cb1));
+ final ImageView timer = ((ImageView)view.findViewById(R.id.image1));
+
+ view.setTag(task);
+ progress.setTag(task);
+
+ name.setText(task.getName());
+
+ if(task.getTimerStart() != null)
+ timer.setImageDrawable(r.getDrawable(R.drawable.ic_dialog_time));
+ progress.setChecked(task.isTaskCompleted());
+ setTaskAppearance(name, task);
+ }
+
+ public void addListeners(final int position, final View view) {
+ final CheckBox progress = ((CheckBox)view.findViewById(R.id.cb1));
+ final TextView name = ((TextView)view.findViewById(R.id.text1));
+
+ // clicking the check box
+ progress.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ TaskModelForList task = (TaskModelForList)buttonView.getTag();
+
+ int newProgressPercentage;
+ if(isChecked)
+ newProgressPercentage = TaskModelForList.getCompletedPercentage();
+ else
+ newProgressPercentage = 0;
+
+ // this takes care of initial checked change
+ if(newProgressPercentage != task.getProgressPercentage()) {
+ task.setProgressPercentage(newProgressPercentage);
+ controller.saveTask(task);
+ setTaskAppearance(name, task);
+ }
+ }
+ });
+
+ // interacting with the text field
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listView.performItemClick(view, position, 0);
+ }
+ });
+
+ view.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ TaskModelForList task = (TaskModelForList)v.getTag();
+ int id = (int)task.getTaskIdentifier().getId();
+
+ menu.add(id, CONTEXT_EDIT_ID, Menu.NONE,
+ R.string.taskList_context_edit);
+ menu.add(id, CONTEXT_DELETE_ID, Menu.NONE,
+ R.string.taskList_context_delete);
+
+ menu.setHeaderTitle(task.getName());
+ }
+ });
+ }
+
+ public void setTaskAppearance(TextView name, TaskModelForList task) {
+ Resources r = getResources();
+
+ if(task.isTaskCompleted()) {
+ name.setBackgroundDrawable(r.getDrawable(R.drawable.strikeout));
+ name.setTextColor(r.getColor(R.color.task_list_done));
+ } else {
+ name.setBackgroundDrawable(null);
+ name.setTextColor(r.getColor(task.getTaskColorResource()));
+ }
+ }
+ }
+
+ // --- ui control handlers
+
+ private void createTask() {
+ Intent i = new Intent(this, TaskEdit.class);
+ startActivityForResult(i, ACTIVITY_CREATE);
+ }
+
+ private void deleteTask(final TaskIdentifier taskId) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.delete_title)
+ .setMessage(R.string.delete_this_task_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ controller.deleteTask(taskId);
+ fillData();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ switch(item.getItemId()) {
+ case INSERT_ID:
+ createTask();
+ return true;
+ case FILTERS_ID:
+ listView.showContextMenu();
+ return true;
+ case TAGS_ID:
+ // TODO
+ return true;
+
+ case CONTEXT_EDIT_ID:
+ long id = item.getGroupId(); // hackhack =(
+
+ Intent intent = new Intent(TaskList.this, TaskEdit.class);
+ intent.putExtra(TaskEdit.LOAD_INSTANCE_TOKEN, id);
+ startActivityForResult(intent, ACTIVITY_EDIT);
+ return true;
+ case CONTEXT_DELETE_ID:
+ id = item.getGroupId();
+
+ deleteTask(new TaskIdentifier(id));
+ return true;
+
+ case CONTEXT_FILTER_HIDDEN:
+ filterShowHidden = !filterShowHidden;
+ fillData();
+ return true;
+ case CONTEXT_FILTER_DONE:
+ filterShowDone = !filterShowDone;
+ fillData();
+ return true;
+ }
+
+ return super.onMenuItemSelected(featureId, item);
+ }
+
+ // --- creating stuff
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.task_list);
+
+ onLoad();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuItem item;
+
+ item = menu.add(Menu.NONE, INSERT_ID, Menu.NONE,
+ R.string.taskList_menu_insert);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+
+ item = menu.add(Menu.NONE, FILTERS_ID, Menu.NONE,
+ R.string.taskList_menu_filters);
+ item.setIcon(android.R.drawable.ic_menu_view);
+ item.setAlphabeticShortcut('f');
+
+ /*item = menu.add(Menu.NONE, TAGS_ID, Menu.NONE,
+ R.string.taskList_menu_tags);
+ item.setIcon(android.R.drawable.ic_menu_myplaces);
+ item.setAlphabeticShortcut('t');*/
+
+ /*item = menu.add(Menu.NONE, SETTINGS_ID, Menu.NONE,
+ R.string.taskList_menu_settings);
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ item.setAlphabeticShortcut('p');*/
+
+ return true;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ controller.close();
+ }
+}
\ No newline at end of file
diff --git a/trunk/src/com/timsu/astrid/activities/TaskModificationActivity.java b/trunk/src/com/timsu/astrid/activities/TaskModificationActivity.java
new file mode 100644
index 000000000..2039fe163
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/activities/TaskModificationActivity.java
@@ -0,0 +1,48 @@
+package com.timsu.astrid.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.timsu.astrid.data.task.AbstractTaskModel;
+import com.timsu.astrid.data.task.TaskController;
+import com.timsu.astrid.data.task.TaskIdentifier;
+
+public abstract class TaskModificationActivity extends Activity {
+ public static final String LOAD_INSTANCE_TOKEN = "id";
+ protected TaskController controller;
+ protected MODEL_TYPE model;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ controller = new TaskController(this);
+ controller.open();
+
+ // check if we have a TaskIdentifier
+ TaskIdentifier identifier = null;
+ Bundle extras = getIntent().getExtras();
+ if(savedInstanceState != null) {
+ identifier = new TaskIdentifier(savedInstanceState.getLong(
+ LOAD_INSTANCE_TOKEN));
+ } else if(extras != null)
+ identifier = new TaskIdentifier(extras.getLong(
+ LOAD_INSTANCE_TOKEN));
+
+ model = getModel(identifier);
+ }
+
+ abstract protected MODEL_TYPE getModel(TaskIdentifier identifier);
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ controller.close();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putLong(LOAD_INSTANCE_TOKEN, model.getTaskIdentifier().getId());
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/activities/TaskView.java b/trunk/src/com/timsu/astrid/activities/TaskView.java
new file mode 100644
index 000000000..4151c371b
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/activities/TaskView.java
@@ -0,0 +1,264 @@
+/*
+ * ASTRID: Android's Simple Task Recording Dame
+ *
+ * Copyright (c) 2009 Tim Su
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.timsu.astrid.activities;
+import java.util.Date;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.timsu.astrid.R;
+import com.timsu.astrid.data.task.TaskIdentifier;
+import com.timsu.astrid.data.task.TaskModelForView;
+import com.timsu.astrid.utilities.DateUtilities;
+import com.timsu.astrid.widget.NumberPicker;
+import com.timsu.astrid.widget.NumberPickerDialog;
+
+
+/** Task Properties view for the Astrid Application.
+ *
+ * @author Tim Su (timsu@stanfordalumni.org)
+ *
+ */
+public class TaskView extends TaskModificationActivity {
+ private static final int ACTIVITY_EDIT = 0;
+
+ private TextView name;
+ private TextView elapsed;
+ private TextView estimated;
+ private TextView definiteDueDate;
+ private TextView preferredDueDate;
+ private TextView notes;
+ private Button timerButton;
+ private Button progress;
+
+ private NumberPickerDialog progressDialog;
+
+ private Handler handler;
+ private Timer updateTimer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.task_view);
+ handler = new Handler();
+
+ setUpUIComponents();
+ setUpListeners();
+ populateFields();
+ }
+
+ @Override
+ protected TaskModelForView getModel(TaskIdentifier identifier) {
+ if(identifier == null)
+ throw new IllegalArgumentException("Can't view null task!");
+ return controller.fetchTaskForView(identifier);
+ }
+
+ private void setUpUIComponents() {
+ Resources r = getResources();
+ setTitle(r.getString(R.string.taskView_title));
+
+ name = (TextView)findViewById(R.id.name);
+ elapsed = (TextView)findViewById(R.id.cell_elapsed);
+ estimated = (TextView)findViewById(R.id.cell_estimated);
+ definiteDueDate = (TextView)findViewById(R.id.cell_definiteDueDate);
+ preferredDueDate = (TextView)findViewById(R.id.cell_preferredDueDate);
+ notes = (TextView)findViewById(R.id.cell_notes);
+ timerButton = (Button)findViewById(R.id.timerButton);
+ progress = (Button)findViewById(R.id.progress);
+
+ progressDialog = new NumberPickerDialog(this,
+ new NumberPickerDialog.OnNumberPickedListener() {
+ @Override
+ public void onNumberPicked(NumberPicker view, int number) {
+ model.setProgressPercentage(number);
+ controller.saveTask(model);
+ updateProgressComponents();
+ }
+ }, r.getString(R.string.progress_dialog), 0, 10, 0, 100);
+
+ name.setTextSize(36);
+
+ final TimerTask elapsedTimeUpdater = new TimerTask() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateElapsedTimeText();
+ }
+ });
+ }
+ };
+
+ // update the UI
+ updateTimer = new Timer();
+ updateTimer.scheduleAtFixedRate(elapsedTimeUpdater, 0, 1000);
+ }
+
+ private void setUpListeners() {
+ Button edit = (Button)findViewById(R.id.edit);
+ edit.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(TaskView.this, TaskEdit.class);
+ intent.putExtra(TaskEdit.LOAD_INSTANCE_TOKEN,
+ model.getTaskIdentifier().getId());
+ startActivityForResult(intent, ACTIVITY_EDIT);
+ }
+ });
+
+ timerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(model.getTimerStart() == null) {
+ model.setTimerStart(new Date());
+ controller.saveTask(model);
+ } else {
+ long start = model.getTimerStart().getTime();
+ model.setTimerStart(null);
+ long secondsElapsed = (System.currentTimeMillis() - start)/1000;
+ model.setElapsedSeconds((int) (model.getElapsedSeconds() +
+ secondsElapsed));
+ controller.saveTask(model);
+ }
+
+ updateTimerButtonText();
+ }
+ });
+ progress.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ progressDialog.show();
+ }
+ });
+ }
+
+ private void populateFields() {
+ final Resources r = getResources();
+ name.setText(model.getName());
+ estimated.setText(DateUtilities.getDurationString(r,
+ model.getEstimatedSeconds(), 2));
+ elapsed.setText(DateUtilities.getDurationString(r,
+ model.getElapsedSeconds(), 2));
+
+ formatDeadline(model.getDefiniteDueDate(), definiteDueDate);
+ formatDeadline(model.getPreferredDueDate(), preferredDueDate);
+
+ updateTimerButtonText();
+ updateProgressComponents();
+
+ if(model.getNotes().length() == 0)
+ ((View)notes.getParent()).setVisibility(View.GONE);
+ else
+ notes.setText(model.getNotes());
+ }
+
+ private void formatDeadline(Date deadline, TextView view) {
+ Resources r = getResources();
+ if(deadline == null || model.isTaskCompleted()) {
+ ((View)view.getParent()).setVisibility(View.GONE);
+ return;
+ }
+
+ int secondsToDeadline = (int) ((deadline.getTime() -
+ System.currentTimeMillis())/1000);
+ String text = DateUtilities.getDurationString(r,
+ Math.abs(secondsToDeadline), 2);
+ if(secondsToDeadline < 0) {
+ view.setTextColor(r.getColor(R.color.view_table_overdue));
+ view.setText(text + r.getString(R.string.overdue_suffix));
+ } else
+ view.setText(text);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent intent) {
+ if(resultCode == RESULT_CANCELED)
+ return;
+
+ // if user edits a task, take them straight to the listing page
+ setResult(resultCode);
+ finish();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ populateFields();
+ }
+
+ /** Update components that depend on elapsed time */
+ private void updateElapsedTimeText() {
+ Resources r = getResources();
+ int timeElapsed = model.getElapsedSeconds();
+ if(model.getTimerStart() != null) {
+ timeElapsed += (int) (System.currentTimeMillis() -
+ model.getTimerStart().getTime())/1000;
+ }
+
+ elapsed.setText(DateUtilities.getDurationString(r,
+ timeElapsed, Integer.MAX_VALUE));
+ }
+
+ /** Update components that depend on timer status */
+ private void updateTimerButtonText() {
+ Resources r = getResources();
+ if(model.getTimerStart() == null)
+ timerButton.setText(r.getString(R.string.startTimer_label));
+ else
+ timerButton.setText(r.getString(R.string.stopTimer_label));
+ }
+
+ /** Update components that depend on task progress */
+ private void updateProgressComponents() {
+ Resources r = getResources();
+ progress.setText(model.getProgressPercentage() +
+ r.getString(R.string.progress_suffix));
+
+ if(model.isTaskCompleted())
+ name.setBackgroundColor(r.getColor(R.color.view_header_done));
+ else
+ name.setBackgroundColor(r.getColor(model.getImportance().getColorResource()));
+
+ progressDialog.setInitialValue(model.getProgressPercentage());
+ }
+
+ @Override
+ /** Cancel the timer thread */
+ protected void onDestroy() {
+ super.onDestroy();
+ updateTimer.cancel();
+ }
+}
+
diff --git a/trunk/src/com/timsu/astrid/data/AbstractController.java b/trunk/src/com/timsu/astrid/data/AbstractController.java
new file mode 100644
index 000000000..dc5d2177a
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/AbstractController.java
@@ -0,0 +1,38 @@
+/*
+ * ASTRID: Android's Simple Task Recording Dame
+ *
+ * Copyright (c) 2009 Tim Su
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.timsu.astrid.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+
+
+abstract public class AbstractController {
+
+ protected Context context;
+ protected SQLiteDatabase database;
+
+ // special columns
+ public static final String KEY_ROWID = "_id";
+
+
+ // database names
+ protected static final String TASK_TABLE_NAME = "tasks";
+
+}
diff --git a/trunk/src/com/timsu/astrid/data/AbstractModel.java b/trunk/src/com/timsu/astrid/data/AbstractModel.java
new file mode 100644
index 000000000..ec64c3a94
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/AbstractModel.java
@@ -0,0 +1,183 @@
+/*
+ * ASTRID: Android's Simple Task Recording Dame
+ *
+ * Copyright (c) 2009 Tim Su
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.timsu.astrid.data;
+
+import java.util.Date;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public abstract class AbstractModel {
+
+ /* Data Source Ordering:
+ *
+ * In order to return the best data, we want to check first what the user
+ * has explicitly set (setValues), then the values we have read out of
+ * the database (values), the database itself (cursor), then defaults
+ * (getDefaultValues)
+ */
+
+ /** User set values */
+ protected ContentValues setValues = new ContentValues();
+
+ /** Cached values from database */
+ private ContentValues values = new ContentValues();
+
+ /** Cursor into the database */
+ private Cursor cursor = null;
+
+ // --- constructors
+
+ /** Construct a model from scratch */
+ public AbstractModel() {
+ // ...
+ }
+
+ /** Construct a model from a database object */
+ public AbstractModel(Cursor cursor) {
+ this.cursor = cursor;
+ }
+
+ // --- data source getters
+
+ /** Get the user-set values for this object */
+ public ContentValues getSetValues() {
+ return setValues;
+ }
+
+ /** Get the default values for this object */
+ abstract public ContentValues getDefaultValues();
+
+ /** Get a list of all field/value pairs merged across data sources */
+ public ContentValues getMergedValues() {
+ ContentValues mergedValues = new ContentValues();
+
+ mergedValues.putAll(getDefaultValues());
+ mergedValues.putAll(values);
+ mergedValues.putAll(setValues);
+
+ return mergedValues;
+ }
+
+ /** Return the database cursor */
+ public Cursor getCursor() {
+ return cursor;
+ }
+
+ // --- data retrieval for the different object types
+
+ protected String retrieveString(String field) {
+ if(setValues.containsKey(field))
+ return setValues.getAsString(field);
+
+ if(values.containsKey(field))
+ return values.getAsString(field);
+
+ // if we have a database to hit, do that now
+ if(cursor != null) {
+ String value = cursor.getString(cursor.getColumnIndexOrThrow(field));
+ values.put(field, value);
+ return value;
+ }
+
+ // do we have defaults?
+ ContentValues defaults = getDefaultValues();
+ if(defaults != null && defaults.containsKey(field))
+ return defaults.getAsString(field);
+
+ throw new UnsupportedOperationException("Could not read field " + field);
+ }
+
+ protected Integer retrieveInteger(String field) {
+ if(setValues.containsKey(field))
+ return setValues.getAsInteger(field);
+
+ if(values.containsKey(field))
+ return values.getAsInteger(field);
+
+ // if we have a database to hit, do that now
+ if(cursor != null) {
+ Integer value = cursor.getInt(cursor.getColumnIndexOrThrow(field));
+ values.put(field, value);
+ return value;
+ }
+
+ // do we have defaults?
+ ContentValues defaults = getDefaultValues();
+ if(defaults != null && defaults.containsKey(field))
+ return defaults.getAsInteger(field);
+
+ throw new UnsupportedOperationException("Could not read field " + field);
+ }
+
+ protected Long retrieveLong(String field) {
+ if(setValues.containsKey(field))
+ return setValues.getAsLong(field);
+
+ if(values.containsKey(field))
+ return values.getAsLong(field);
+
+ // if we have a database to hit, do that now
+ if(cursor != null) {
+ Long value = cursor.getLong(cursor.getColumnIndexOrThrow(field));
+ values.put(field, value);
+ return value;
+ }
+
+ // do we have defaults?
+ ContentValues defaults = getDefaultValues();
+ if(defaults != null && defaults.containsKey(field))
+ return defaults.getAsLong(field);
+
+ throw new UnsupportedOperationException("Could not read field " + field);
+ }
+
+ protected Double retrieveDouble(String field) {
+ if(setValues.containsKey(field))
+ return setValues.getAsDouble(field);
+
+ if(values.containsKey(field))
+ return values.getAsDouble(field);
+
+ // if we have a database to hit, do that now
+ if(cursor != null) {
+ Double value = cursor.getDouble(cursor.getColumnIndexOrThrow(field));
+ values.put(field, value);
+ return value;
+ }
+
+ // do we have defaults?
+ ContentValues defaults = getDefaultValues();
+ if(defaults != null && defaults.containsKey(field))
+ return defaults.getAsDouble(field);
+
+ throw new UnsupportedOperationException("Could not read field " + field);
+ }
+
+ // --- retrieving composite objects
+
+ protected Date retrieveDate(String field) {
+ Long time = retrieveLong(field);
+ if(time == null || time == 0)
+ return null;
+
+ return new Date(time);
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/enums/Importance.java b/trunk/src/com/timsu/astrid/data/enums/Importance.java
new file mode 100644
index 000000000..4ca7d4dcf
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/enums/Importance.java
@@ -0,0 +1,32 @@
+package com.timsu.astrid.data.enums;
+
+import com.timsu.astrid.R;
+
+public enum Importance {
+ // MOST IMPORTANT
+
+ LEVEL_1(R.string.importance_1, R.color.importance_1),
+ LEVEL_2(R.string.importance_2, R.color.importance_2),
+ LEVEL_3(R.string.importance_3, R.color.importance_3),
+ LEVEL_4(R.string.importance_4, R.color.importance_4),
+
+ // LEAST IMPORTANT
+ ;
+
+ int label;
+ int color;
+ public static final Importance DEFAULT = LEVEL_2;
+
+ private Importance(int label, int color) {
+ this.label = label;
+ this.color = color;
+ }
+
+ public int getLabelResource() {
+ return label;
+ }
+
+ public int getColorResource() {
+ return color;
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/location/GeoPoint.java b/trunk/src/com/timsu/astrid/data/location/GeoPoint.java
new file mode 100644
index 000000000..6490024b8
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/location/GeoPoint.java
@@ -0,0 +1,20 @@
+package com.timsu.astrid.data.location;
+
+public class GeoPoint {
+
+ private int latitude, longitude;
+
+ public GeoPoint(int latitude, int longitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
+ public int getLatitudeE6() {
+ return latitude;
+ }
+
+ public int getLongitudeE6() {
+ return longitude;
+ }
+
+}
diff --git a/trunk/src/com/timsu/astrid/data/task/AbstractTaskModel.java b/trunk/src/com/timsu/astrid/data/task/AbstractTaskModel.java
new file mode 100644
index 000000000..8d205c7c9
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/task/AbstractTaskModel.java
@@ -0,0 +1,301 @@
+package com.timsu.astrid.data.task;
+
+import java.util.Date;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import com.timsu.astrid.R;
+import com.timsu.astrid.data.AbstractController;
+import com.timsu.astrid.data.AbstractModel;
+import com.timsu.astrid.data.enums.Importance;
+
+
+/** Abstract model of a task. Subclasses implement the getters and setters
+ * they are interested in.
+ *
+ * @author timsu
+ *
+ */
+public abstract class AbstractTaskModel extends AbstractModel {
+
+ /** Version number of this model */
+ static final int VERSION = 1;
+
+ public static final int COMPLETE_PERCENTAGE = 100;
+
+ // 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 BLOCKING_ON = "blockingOn";
+ static final String NOTIFICATIONS = "notifications";
+ static final String CREATION_DATE = "creationDate";
+ static final String COMPLETION_DATE = "completionDate";
+
+ /** Default values container */
+ private static final ContentValues defaultValues = new ContentValues();
+
+ static {
+ defaultValues.put(NAME, "");
+ defaultValues.put(NOTES, "");
+ defaultValues.put(PROGRESS_PERCENTAGE, 0);
+ defaultValues.put(IMPORTANCE, Importance.DEFAULT.ordinal());
+ defaultValues.put(ESTIMATED_SECONDS, 0);
+ defaultValues.put(ELAPSED_SECONDS, 0);
+ defaultValues.put(TIMER_START, (Long)null);
+ defaultValues.put(DEFINITE_DUE_DATE, (Long)null);
+ defaultValues.put(PREFERRED_DUE_DATE, (Long)null);
+ defaultValues.put(HIDDEN_UNTIL, (Long)null);
+ defaultValues.put(BLOCKING_ON, (Long)null);
+ defaultValues.put(NOTIFICATIONS, 0);
+ defaultValues.put(COMPLETION_DATE, (Long)null);
+ }
+
+ @Override
+ public ContentValues getDefaultValues() {
+ return defaultValues;
+ }
+
+ // --- database helper
+
+ /** Database Helper manages creating new tables and updating old ones */
+ static class TaskModelDatabaseHelper extends SQLiteOpenHelper {
+ String tableName;
+
+ TaskModelDatabaseHelper(Context context, String databaseName) {
+ super(context, databaseName, null, VERSION);
+
+ this.tableName = databaseName;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ String sql = new StringBuilder().
+ append("CREATE TABLE ").append(tableName).append(" (").
+ append(AbstractController.KEY_ROWID).append(" integer primary key autoincrement, ").
+ append(NAME).append(" text not null,").
+ append(NOTES).append(" text not null,").
+ append(PROGRESS_PERCENTAGE).append(" integer not null,").
+ append(IMPORTANCE).append(" integer not null,").
+ append(ESTIMATED_SECONDS).append(" integer,").
+ append(ELAPSED_SECONDS).append(" integer,").
+ append(TIMER_START).append(" integer,").
+ append(DEFINITE_DUE_DATE).append(" integer,").
+ append(PREFERRED_DUE_DATE).append(" integer,").
+ append(HIDDEN_UNTIL).append(" integer,").
+ append(BLOCKING_ON).append(" integer,").
+ append(NOTIFICATIONS).append(" integer,").
+ append(CREATION_DATE).append(" integer,").
+ append(COMPLETION_DATE).append(" integer").
+ append(");").toString();
+ db.execSQL(sql);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(getClass().getSimpleName(), "Upgrading database from version " +
+ oldVersion + " to " + newVersion + ".");
+
+ switch(oldVersion) {
+ default:
+ // we don't know how to handle it... do the unfortunate thing
+ Log.e(getClass().getSimpleName(), "Unsupported migration, table dropped!");
+ db.execSQL("DROP TABLE IF EXISTS " + tableName);
+ onCreate(db);
+ }
+ }
+ }
+
+ // --- utility methods
+
+ /** Gets task color. Requires definiteDueDate and importance */
+ protected int getTaskColorResource() {
+ if(getDefiniteDueDate() != null && getDefiniteDueDate().getTime() <
+ System.currentTimeMillis()) {
+ return R.color.task_list_overdue;
+ } else {
+ return getImportance().getColorResource();
+ }
+ }
+
+ /** Checks whether task is done. Requires progressPercentage */
+ protected boolean isTaskCompleted() {
+ return getProgressPercentage() >= COMPLETE_PERCENTAGE;
+ }
+
+ // --- task identifier
+
+ private TaskIdentifier identifier = null;
+
+ public TaskIdentifier getTaskIdentifier() {
+ return identifier;
+ }
+
+ void setTaskIdentifier(TaskIdentifier identifier) {
+ this.identifier = identifier;
+ }
+
+ // --- constructor pass-through
+
+ AbstractTaskModel() {
+ super();
+ }
+
+ /** Read identifier from database */
+ AbstractTaskModel(Cursor cursor) {
+ super(cursor);
+
+ Integer id = retrieveInteger(AbstractController.KEY_ROWID);
+ setTaskIdentifier(new TaskIdentifier(id));
+ }
+
+ /** Get identifier from argument */
+ AbstractTaskModel(TaskIdentifier identifier, Cursor cursor) {
+ super(cursor);
+
+ setTaskIdentifier(identifier);
+ }
+
+ // --- getters and setters: expose them as you see fit
+
+ protected String getName() {
+ return retrieveString(NAME);
+ }
+
+ protected String getNotes() {
+ return retrieveString(NOTES);
+ }
+
+ protected int getProgressPercentage() {
+ return retrieveInteger(PROGRESS_PERCENTAGE);
+ }
+
+ protected Importance getImportance() {
+ Integer value = retrieveInteger(IMPORTANCE);
+ if(value == null)
+ return null;
+ return Importance.values()[value];
+ }
+
+ protected Integer getEstimatedSeconds() {
+ return retrieveInteger(ESTIMATED_SECONDS);
+ }
+
+ protected Integer getElapsedSeconds() {
+ return retrieveInteger(ELAPSED_SECONDS);
+ }
+
+ protected Date getTimerStart() {
+ return retrieveDate(TIMER_START);
+ }
+
+ protected Date getDefiniteDueDate() {
+ return retrieveDate(DEFINITE_DUE_DATE);
+ }
+
+ protected Date getPreferredDueDate() {
+ return retrieveDate(PREFERRED_DUE_DATE);
+ }
+
+ protected Date getHiddenUntil() {
+ return retrieveDate(HIDDEN_UNTIL);
+ }
+
+ protected Date getCreationDate() {
+ return retrieveDate(CREATION_DATE);
+ }
+
+ protected Date getCompletionDate() {
+ return retrieveDate(COMPLETION_DATE);
+ }
+
+ protected TaskIdentifier getBlockingOn() {
+ Long value = retrieveLong(BLOCKING_ON);
+ if(value == null)
+ return null;
+ return new TaskIdentifier(value);
+ }
+
+ // --- setters
+
+ protected void setName(String name) {
+ setValues.put(NAME, name);
+ }
+
+ protected void setNotes(String notes) {
+ setValues.put(NOTES, notes);
+ }
+
+ protected void setProgressPercentage(int progressPercentage) {
+ setValues.put(PROGRESS_PERCENTAGE, progressPercentage);
+
+ if(getProgressPercentage() != progressPercentage &&
+ progressPercentage == COMPLETE_PERCENTAGE)
+ setCompletionDate(new Date());
+ }
+
+ protected void setImportance(Importance importance) {
+ setValues.put(IMPORTANCE, importance.ordinal());
+ }
+
+ protected void setEstimatedSeconds(Integer estimatedSeconds) {
+ setValues.put(ESTIMATED_SECONDS, estimatedSeconds);
+ }
+
+ protected void setElapsedSeconds(int elapsedSeconds) {
+ setValues.put(ELAPSED_SECONDS, elapsedSeconds);
+ }
+
+ protected void setTimerStart(Date timerStart) {
+ putDate(setValues, TIMER_START, timerStart);
+ }
+
+ protected void setDefiniteDueDate(Date definiteDueDate) {
+ putDate(setValues, DEFINITE_DUE_DATE, definiteDueDate);
+ }
+
+ protected void setPreferredDueDate(Date preferredDueDate) {
+ putDate(setValues, PREFERRED_DUE_DATE, preferredDueDate);
+ }
+
+ protected void setHiddenUntil(Date hiddenUntil) {
+ putDate(setValues, HIDDEN_UNTIL, hiddenUntil);
+ }
+
+ protected void setBlockingOn(TaskIdentifier blockingOn) {
+ if(blockingOn == null)
+ setValues.put(BLOCKING_ON, (Integer)null);
+ else
+ setValues.put(BLOCKING_ON, blockingOn.getId());
+ }
+
+ protected void setCreationDate(Date creationDate) {
+ putDate(setValues, CREATION_DATE, creationDate);
+ }
+
+ protected void setCompletionDate(Date completionDate) {
+ putDate(setValues, COMPLETION_DATE, completionDate);
+ }
+
+ // --- utility methods
+
+ static void putDate(ContentValues cv, String fieldName, Date date) {
+ if(date == null)
+ cv.put(fieldName, (Long)null);
+ else
+ cv.put(fieldName, date.getTime());
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/task/TaskController.java b/trunk/src/com/timsu/astrid/data/task/TaskController.java
new file mode 100644
index 000000000..6154a07f6
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/task/TaskController.java
@@ -0,0 +1,154 @@
+package com.timsu.astrid.data.task;
+
+import java.util.Date;
+import java.util.List;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.timsu.astrid.data.AbstractController;
+import com.timsu.astrid.data.task.AbstractTaskModel.TaskModelDatabaseHelper;
+
+public class TaskController extends AbstractController {
+
+ // --- task list operations
+
+ /** Return a list of all of the tasks with progress < COMPLETE_PERCENTAGE */
+ public Cursor getActiveTaskListCursor() {
+ return database.query(TASK_TABLE_NAME, TaskModelForList.FIELD_LIST,
+ AbstractTaskModel.PROGRESS_PERCENTAGE + " < " +
+ AbstractTaskModel.COMPLETE_PERCENTAGE, null, null, null,
+ null, null);
+ }
+
+ /** Return a list of all tasks */
+ public Cursor getAllTaskListCursor() {
+ return database.query(TASK_TABLE_NAME, TaskModelForList.FIELD_LIST,
+ null, null, null, null, null, null);
+ }
+
+ /** Create a weighted list of tasks from the db cursor given */
+ public List createTaskListFromCursor(Cursor cursor,
+ boolean hideHidden) {
+ return TaskModelForList.createTaskModelList(cursor, hideHidden);
+ }
+
+ // --- single task operations
+
+ /** Delete the given task */
+ public boolean deleteTask(TaskIdentifier taskId) {
+ if(taskId == null)
+ throw new UnsupportedOperationException("Cannot delete uncreated task!");
+ long id = taskId.getId();
+ return database.delete(TASK_TABLE_NAME, KEY_ROWID + "=" + id, null) > 0;
+ }
+
+ /** Sets the timer start time to the given value. Passing in "null"
+ * signifies that the timer is not running. */
+ public boolean startTimer(TaskModelForView task, Date startDate) throws
+ SQLException {
+ task.setTimerStart(startDate);
+
+ long id = task.getTaskIdentifier().getId();
+ ContentValues values = new ContentValues();
+ AbstractTaskModel.putDate(values, AbstractTaskModel.TIMER_START,
+ startDate);
+ return database.update(TASK_TABLE_NAME, values, KEY_ROWID + "=" + id,
+ null) > 0;
+ }
+
+ /** Saves the given task to the database. Returns true on success. */
+ public boolean saveTask(AbstractTaskModel task) {
+ boolean saveSucessful;
+
+ if(task.getTaskIdentifier() == null) {
+ saveSucessful = database.insert(TASK_TABLE_NAME, AbstractTaskModel.NAME,
+ task.getMergedValues()) >= 0;
+ } else {
+ long id = task.getTaskIdentifier().getId();
+ saveSucessful = database.update(TASK_TABLE_NAME, task.getSetValues(),
+ KEY_ROWID + "=" + id, null) > 0;
+ }
+
+ return saveSucessful;
+ }
+
+ // --- fetching different models
+
+ /** Creates a new task and returns the task identifier */
+ public TaskModelForEdit createNewTaskForEdit() {
+ TaskModelForEdit task = new TaskModelForEdit();
+ task.setTaskIdentifier(null);
+
+ return task;
+ }
+
+ /** Returns a TaskModelForEdit corresponding to the given TaskIdentifier */
+ public TaskModelForEdit fetchTaskForEdit(TaskIdentifier taskId) throws SQLException {
+ long id = taskId.getId();
+ Cursor cursor = database.query(true, TASK_TABLE_NAME,
+ TaskModelForEdit.FIELD_LIST,
+ KEY_ROWID + "=" + id, null, null, null, null, null);
+
+ if (cursor != null) {
+ cursor.moveToFirst();
+ TaskModelForEdit model = new TaskModelForEdit(taskId, cursor);
+ return model;
+ }
+
+ throw new SQLException("Returned empty set!");
+
+ }
+
+
+ /** Returns a TaskModelForView corresponding to the given TaskIdentifier */
+ public TaskModelForView fetchTaskForView(TaskIdentifier taskId) throws SQLException {
+ long id = taskId.getId();
+ Cursor cursor = database.query(true, TASK_TABLE_NAME,
+ TaskModelForView.FIELD_LIST,
+ KEY_ROWID + "=" + id, null, null, null, null, null);
+
+ if (cursor != null) {
+ cursor.moveToFirst();
+ TaskModelForView model = new TaskModelForView(taskId, cursor);
+ return model;
+ }
+
+ throw new SQLException("Returned empty set!");
+
+ }
+
+ // --- boilerplate
+
+ /**
+ * Constructor - takes the context to allow the database to be
+ * opened/created
+ */
+ public TaskController(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Open the notes database. If it cannot be opened, try to create a new
+ * instance of the database. If it cannot be created, throw an exception to
+ * signal the failure
+ *
+ * @return this (self reference, allowing this to be chained in an
+ * initialization call)
+ * @throws SQLException if the database could be neither opened or created
+ */
+ public TaskController open() throws SQLException {
+ SQLiteOpenHelper databaseHelper = new TaskModelDatabaseHelper(
+ context, TASK_TABLE_NAME);
+ database = databaseHelper.getWritableDatabase();
+ return this;
+ }
+
+ /** Closes database resource */
+ public void close() {
+ database.close();
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/task/TaskIdentifier.java b/trunk/src/com/timsu/astrid/data/task/TaskIdentifier.java
new file mode 100644
index 000000000..5d1135789
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/task/TaskIdentifier.java
@@ -0,0 +1,15 @@
+package com.timsu.astrid.data.task;
+
+
+/** A little class that identifies a task. For saving state and passing around */
+public class TaskIdentifier {
+ private long id;
+
+ public TaskIdentifier(long id) {
+ this.id = id;
+ }
+
+ public long getId() {
+ return id;
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/task/TaskModelForEdit.java b/trunk/src/com/timsu/astrid/data/task/TaskModelForEdit.java
new file mode 100644
index 000000000..3efb6cdb6
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/task/TaskModelForEdit.java
@@ -0,0 +1,128 @@
+package com.timsu.astrid.data.task;
+
+import java.util.Date;
+
+import com.timsu.astrid.data.enums.Importance;
+
+import android.database.Cursor;
+
+
+
+/** Fields that you would want to edit in the TaskModel */
+public class TaskModelForEdit extends AbstractTaskModel {
+
+ static String[] FIELD_LIST = new String[] {
+ NAME,
+ IMPORTANCE,
+ ESTIMATED_SECONDS,
+ ELAPSED_SECONDS,
+ DEFINITE_DUE_DATE,
+ PREFERRED_DUE_DATE,
+ HIDDEN_UNTIL,
+ BLOCKING_ON,
+ NOTES,
+ };
+
+ // --- constructors
+
+ public TaskModelForEdit() {
+ super();
+ setCreationDate(new Date());
+ }
+
+ public TaskModelForEdit(TaskIdentifier identifier, Cursor cursor) {
+ super(identifier, cursor);
+ }
+
+ // --- getters and setters
+
+ @Override
+ public Date getDefiniteDueDate() {
+ return super.getDefiniteDueDate();
+ }
+
+ @Override
+ public Integer getEstimatedSeconds() {
+ return super.getEstimatedSeconds();
+ }
+
+ @Override
+ public Integer getElapsedSeconds() {
+ return super.getElapsedSeconds();
+ }
+
+ @Override
+ public Date getHiddenUntil() {
+ return super.getHiddenUntil();
+ }
+
+ @Override
+ public Importance getImportance() {
+ return super.getImportance();
+ }
+
+ @Override
+ public String getName() {
+ return super.getName();
+ }
+
+ @Override
+ public String getNotes() {
+ return super.getNotes();
+ }
+
+ @Override
+ public Date getPreferredDueDate() {
+ return super.getPreferredDueDate();
+ }
+
+ @Override
+ public TaskIdentifier getBlockingOn() {
+ return super.getBlockingOn();
+ }
+
+ @Override
+ public void setDefiniteDueDate(Date definiteDueDate) {
+ super.setDefiniteDueDate(definiteDueDate);
+ }
+
+ @Override
+ public void setEstimatedSeconds(Integer estimatedSeconds) {
+ super.setEstimatedSeconds(estimatedSeconds);
+ }
+
+ @Override
+ public void setElapsedSeconds(int elapsedSeconds) {
+ super.setElapsedSeconds(elapsedSeconds);
+ }
+
+ @Override
+ public void setHiddenUntil(Date hiddenUntil) {
+ super.setHiddenUntil(hiddenUntil);
+ }
+
+ @Override
+ public void setImportance(Importance importance) {
+ super.setImportance(importance);
+ }
+
+ @Override
+ public void setName(String name) {
+ super.setName(name);
+ }
+
+ @Override
+ public void setNotes(String notes) {
+ super.setNotes(notes);
+ }
+
+ @Override
+ public void setPreferredDueDate(Date preferredDueDate) {
+ super.setPreferredDueDate(preferredDueDate);
+ }
+
+ @Override
+ public void setBlockingOn(TaskIdentifier blockingOn) {
+ super.setBlockingOn(blockingOn);
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/task/TaskModelForList.java b/trunk/src/com/timsu/astrid/data/task/TaskModelForList.java
new file mode 100644
index 000000000..732b0ffa6
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/task/TaskModelForList.java
@@ -0,0 +1,183 @@
+package com.timsu.astrid.data.task;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+
+import android.database.Cursor;
+
+import com.timsu.astrid.data.AbstractController;
+import com.timsu.astrid.data.enums.Importance;
+
+
+
+/** Fields that you would want to edit in the TaskModel */
+public class TaskModelForList extends AbstractTaskModel {
+
+ static String[] FIELD_LIST = new String[] {
+ AbstractController.KEY_ROWID,
+ NAME,
+ IMPORTANCE,
+ ELAPSED_SECONDS,
+ ESTIMATED_SECONDS,
+ TIMER_START,
+ DEFINITE_DUE_DATE,
+ PREFERRED_DUE_DATE,
+ PROGRESS_PERCENTAGE,
+ HIDDEN_UNTIL,
+ };
+
+ static List createTaskModelList(Cursor cursor,
+ boolean hideHidden) {
+ ArrayList list = new ArrayList();
+ final HashMap weights = new
+ HashMap();
+
+ // first, load everything
+ for(int i = 0; i < cursor.getCount(); i++) {
+ cursor.moveToNext();
+ TaskModelForList task = new TaskModelForList(cursor);
+
+ // hide tasks
+ if(hideHidden) {
+ if(task.getHiddenUntil() != null &&
+ task.getHiddenUntil().getTime() > System.currentTimeMillis())
+ continue;
+ }
+
+ list.add(task);
+ weights.put(task, task.getWeight());
+ }
+
+ // now sort
+ Collections.sort(list, new Comparator() {
+ @Override
+ public int compare(TaskModelForList a, TaskModelForList b) {
+ return weights.get(a) - weights.get(b);
+ }
+ });
+
+ return list;
+ }
+
+ /** Get the weighted score for this task. Smaller is more important */
+ private int getWeight() {
+ int weight = 0;
+
+ // importance
+ weight += getImportance().ordinal() * 60;
+
+ // estimated time left
+ int secondsLeft = getEstimatedSeconds() - getElapsedSeconds();
+ if(secondsLeft > 0)
+ weight += secondsLeft / 120;
+
+ // looming absolute deadline
+ if(getDefiniteDueDate() != null) {
+ int hoursLeft = (int) (getDefiniteDueDate().getTime() -
+ System.currentTimeMillis())/1000/3600;
+ if(hoursLeft < 5*24)
+ weight += (hoursLeft - 5*24);
+ }
+
+ // looming preferred deadline
+ if(getPreferredDueDate() != null) {
+ int hoursLeft = (int) (getPreferredDueDate().getTime() -
+ System.currentTimeMillis())/1000/3600;
+ if(hoursLeft < 5*24)
+ weight += (hoursLeft - 5*24)/2;
+ }
+
+ // bubble completed tasks to the bottom
+ if(isTaskCompleted())
+ weight += 1000;
+
+ return weight;
+ }
+
+ // --- constructors
+
+ public TaskModelForList(Cursor cursor) {
+ super(cursor);
+
+ // prefetch every field - we can't lazy load with more than 1
+ getElapsedSeconds();
+ getDefiniteDueDate();
+ getEstimatedSeconds();
+ getHiddenUntil();
+ getImportance();
+ getName();
+ getPreferredDueDate();
+ getProgressPercentage();
+ getTimerStart();
+ }
+
+ // --- getters and setters
+
+ @Override
+ public boolean isTaskCompleted() {
+ return super.isTaskCompleted();
+ }
+
+ @Override
+ public int getTaskColorResource() {
+ return super.getTaskColorResource();
+ }
+
+ @Override
+ public Integer getElapsedSeconds() {
+ return super.getElapsedSeconds();
+ }
+
+ public static int getCompletedPercentage() {
+ return COMPLETE_PERCENTAGE;
+ }
+
+ @Override
+ public Date getDefiniteDueDate() {
+ return super.getDefiniteDueDate();
+ }
+
+ @Override
+ public Integer getEstimatedSeconds() {
+ return super.getEstimatedSeconds();
+ }
+
+ @Override
+ public Date getHiddenUntil() {
+ return super.getHiddenUntil();
+ }
+
+ @Override
+ public Importance getImportance() {
+ return super.getImportance();
+ }
+
+ @Override
+ public String getName() {
+ return super.getName();
+ }
+
+ @Override
+ public Date getPreferredDueDate() {
+ return super.getPreferredDueDate();
+ }
+
+ @Override
+ public int getProgressPercentage() {
+ return super.getProgressPercentage();
+ }
+
+ @Override
+ public Date getTimerStart() {
+ return super.getTimerStart();
+ }
+
+ @Override
+ public void setProgressPercentage(int progressPercentage) {
+ super.setProgressPercentage(progressPercentage);
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/data/task/TaskModelForView.java b/trunk/src/com/timsu/astrid/data/task/TaskModelForView.java
new file mode 100644
index 000000000..8b88a7633
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/data/task/TaskModelForView.java
@@ -0,0 +1,105 @@
+package com.timsu.astrid.data.task;
+
+import java.util.Date;
+
+import android.database.Cursor;
+
+import com.timsu.astrid.data.AbstractController;
+import com.timsu.astrid.data.enums.Importance;
+
+
+
+/** Fields that you would want to see in the TaskView activity */
+public class TaskModelForView extends AbstractTaskModel {
+
+ static String[] FIELD_LIST = new String[] {
+ AbstractController.KEY_ROWID,
+ NAME,
+ IMPORTANCE,
+ PROGRESS_PERCENTAGE,
+ ESTIMATED_SECONDS,
+ ELAPSED_SECONDS,
+ TIMER_START,
+ DEFINITE_DUE_DATE,
+ PREFERRED_DUE_DATE,
+ NOTES,
+ };
+
+ // --- constructors
+
+ public TaskModelForView(TaskIdentifier identifier, Cursor cursor) {
+ super(identifier, cursor);
+ }
+
+ // --- getters and setters
+
+ @Override
+ public boolean isTaskCompleted() {
+ return super.isTaskCompleted();
+ }
+
+ @Override
+ public int getTaskColorResource() {
+ return super.getTaskColorResource();
+ }
+
+ @Override
+ public int getProgressPercentage() {
+ return super.getProgressPercentage();
+ }
+
+ @Override
+ public Date getDefiniteDueDate() {
+ return super.getDefiniteDueDate();
+ }
+
+ @Override
+ public Integer getEstimatedSeconds() {
+ return super.getEstimatedSeconds();
+ }
+
+ @Override
+ public Integer getElapsedSeconds() {
+ return super.getElapsedSeconds();
+ }
+
+ @Override
+ public Importance getImportance() {
+ return super.getImportance();
+ }
+
+ @Override
+ public String getName() {
+ return super.getName();
+ }
+
+ @Override
+ public String getNotes() {
+ return super.getNotes();
+ }
+
+ @Override
+ public Date getPreferredDueDate() {
+ return super.getPreferredDueDate();
+ }
+
+ @Override
+ public Date getTimerStart() {
+ return super.getTimerStart();
+ }
+
+ @Override
+ public void setTimerStart(Date timerStart) {
+ super.setTimerStart(timerStart);
+ }
+
+ @Override
+ public void setProgressPercentage(int progressPercentage) {
+ super.setProgressPercentage(progressPercentage);
+ }
+
+ @Override
+ public void setElapsedSeconds(int elapsedSeconds) {
+ super.setElapsedSeconds(elapsedSeconds);
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/utilities/DateUtilities.java b/trunk/src/com/timsu/astrid/utilities/DateUtilities.java
new file mode 100644
index 000000000..56cd31dc6
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/utilities/DateUtilities.java
@@ -0,0 +1,102 @@
+package com.timsu.astrid.utilities;
+
+import android.content.res.Resources;
+
+import com.timsu.astrid.R;
+
+public class DateUtilities {
+
+ /**
+ * Format a time into the format: 5 days, 3 hours, 2 minutes
+ *
+ * @param r Resources to get strings from
+ * @param timeInSeconds
+ * @param unitsToShow number of units to show (i.e. if 2, then 5 hours
+ * 3 minutes 2 seconds is truncated to 5 hours 3 minutes)
+ * @return
+ */
+ public static String getDurationString(Resources r, int timeInSeconds,
+ int unitsToShow) {
+ short days, hours, minutes, seconds;
+ short unitsDisplayed = 0;
+
+ if(timeInSeconds == 0)
+ return r.getQuantityString(R.plurals.Nseconds, 0, 0);
+
+ days = (short)(timeInSeconds / 24 / 3600);
+ timeInSeconds -= days*24*3600;
+ hours = (short)(timeInSeconds / 3600);
+ timeInSeconds -= hours * 3600;
+ minutes = (short)(timeInSeconds / 60);
+ timeInSeconds -= minutes * 60;
+ seconds = (short)timeInSeconds;
+
+ StringBuilder result = new StringBuilder();
+ if(days > 0) {
+ result.append(r.getQuantityString(R.plurals.Ndays, days, days)).
+ append(" ");
+ unitsDisplayed++;
+ }
+ if(unitsDisplayed < unitsToShow && hours > 0) {
+ result.append(r.getQuantityString(R.plurals.Nhours, hours,
+ hours)).
+ append(" ");
+ unitsDisplayed++;
+ }
+ if(unitsDisplayed < unitsToShow && minutes > 0) {
+ result.append(r.getQuantityString(R.plurals.Nminutes, minutes,
+ minutes)).append(" ");
+ unitsDisplayed++;
+ }
+ if(unitsDisplayed < unitsToShow && seconds > 0) {
+ result.append(r.getQuantityString(R.plurals.Nseconds, seconds,
+ seconds)).append(" ");
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Format a time into the format: 5 d, 3 h, 2 m
+ *
+ * @param r Resources to get strings from
+ * @param timeInSeconds
+ * @param unitsToShow number of units to show
+ * @return
+ */
+ public static String getShortDurationString(Resources r, int timeInSeconds,
+ int unitsToShow) {
+ short days, hours, minutes, seconds;
+ short unitsDisplayed = 0;
+
+ if(timeInSeconds == 0)
+ return "0 s";
+
+ days = (short)(timeInSeconds / 24 / 3600);
+ timeInSeconds -= days*24*3600;
+ hours = (short)(timeInSeconds / 3600);
+ timeInSeconds -= hours * 3600;
+ minutes = (short)(timeInSeconds / 60);
+ timeInSeconds -= minutes * 60;
+ seconds = (short)timeInSeconds;
+
+ StringBuilder result = new StringBuilder();
+ if(days > 0) {
+ result.append(days).append(" d ");
+ unitsDisplayed++;
+ }
+ if(unitsDisplayed < unitsToShow && hours > 0) {
+ result.append(hours).append(" h ");
+ unitsDisplayed++;
+ }
+ if(unitsDisplayed < unitsToShow && minutes > 0) {
+ result.append(minutes).append(" m ");
+ unitsDisplayed++;
+ }
+ if(unitsDisplayed < unitsToShow && seconds < 0) {
+ result.append(hours).append(" s ");
+ }
+
+ return result.toString();
+ }
+}
diff --git a/trunk/src/com/timsu/astrid/widget/NumberPicker.java b/trunk/src/com/timsu/astrid/widget/NumberPicker.java
new file mode 100644
index 000000000..2ceae7799
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/widget/NumberPicker.java
@@ -0,0 +1,422 @@
+package com.timsu.astrid.widget;
+
+import android.content.Context;
+import android.os.Handler;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.method.NumberKeyListener;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnLongClickListener;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.timsu.astrid.R;
+
+public class NumberPicker extends LinearLayout implements OnClickListener,
+ OnFocusChangeListener, OnLongClickListener {
+
+ public interface OnChangedListener {
+ void onChanged(NumberPicker picker, int oldVal, int newVal);
+ }
+
+ public interface Formatter {
+ String toString(int value);
+ }
+
+ /*
+ * Use a custom NumberPicker formatting callback to use two-digit minutes
+ * strings like "01". Keeping a static formatter etc. is the most efficient
+ * way to do this; it avoids creating temporary objects on every call to
+ * format().
+ */
+ public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
+ final StringBuilder mBuilder = new StringBuilder();
+ final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
+ final Object[] mArgs = new Object[1];
+
+ public String toString(int value) {
+ mArgs[0] = value;
+ mBuilder.delete(0, mBuilder.length());
+ mFmt.format("%02d", mArgs);
+ return mFmt.toString();
+ }
+ };
+
+ private int incrementBy;
+ public void setIncrementBy(int incrementBy) {
+ this.incrementBy = incrementBy;
+ }
+
+ private final Handler mHandler;
+ private final Runnable mRunnable = new Runnable() {
+ public void run() {
+ if (mIncrement) {
+ changeCurrent(mCurrent + incrementBy, mSlideUpInAnimation, mSlideUpOutAnimation);
+ mHandler.postDelayed(this, mSpeed);
+ } else if (mDecrement) {
+ changeCurrent(mCurrent - incrementBy, mSlideDownInAnimation, mSlideDownOutAnimation);
+ mHandler.postDelayed(this, mSpeed);
+ }
+ }
+ };
+
+ private final LayoutInflater mInflater;
+ private final TextView mText;
+ private final InputFilter mInputFilter;
+ private final InputFilter mNumberInputFilter;
+
+ private final Animation mSlideUpOutAnimation;
+ private final Animation mSlideUpInAnimation;
+ private final Animation mSlideDownOutAnimation;
+ private final Animation mSlideDownInAnimation;
+
+ private String[] mDisplayedValues;
+ private int mStart;
+ private int mEnd;
+ private int mCurrent;
+ private int mPrevious;
+ private OnChangedListener mListener;
+ private Formatter mFormatter;
+ private long mSpeed = 300;
+
+ private boolean mIncrement;
+ private boolean mDecrement;
+
+ public NumberPicker(Context context, int incrementBy) {
+ this(context, null);
+ }
+
+ public NumberPicker(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs);
+ setOrientation(VERTICAL);
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mInflater.inflate(R.layout.number_picker, this, true);
+ mHandler = new Handler();
+ mInputFilter = new NumberPickerInputFilter();
+ mNumberInputFilter = new NumberRangeKeyListener();
+ mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
+ mIncrementButton.setOnClickListener(this);
+ mIncrementButton.setOnLongClickListener(this);
+ mIncrementButton.setNumberPicker(this);
+ mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
+ mDecrementButton.setOnClickListener(this);
+ mDecrementButton.setOnLongClickListener(this);
+ mDecrementButton.setNumberPicker(this);
+
+ mText = (TextView) findViewById(R.id.timepicker_input);
+ mText.setOnFocusChangeListener(this);
+ mText.setFilters(new InputFilter[] { mInputFilter });
+
+ mSlideUpOutAnimation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
+ Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, -100);
+ mSlideUpOutAnimation.setDuration(200);
+ mSlideUpInAnimation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
+ Animation.RELATIVE_TO_SELF, 100, Animation.RELATIVE_TO_SELF, 0);
+ mSlideUpInAnimation.setDuration(200);
+ mSlideDownOutAnimation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
+ Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 100);
+ mSlideDownOutAnimation.setDuration(200);
+ mSlideDownInAnimation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
+ Animation.RELATIVE_TO_SELF, -100, Animation.RELATIVE_TO_SELF, 0);
+ mSlideDownInAnimation.setDuration(200);
+
+ if (!isEnabled()) {
+ setEnabled(false);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mIncrementButton.setEnabled(enabled);
+ mDecrementButton.setEnabled(enabled);
+ mText.setEnabled(enabled);
+ }
+
+ public void setOnChangeListener(OnChangedListener listener) {
+ mListener = listener;
+ }
+
+ public void setFormatter(Formatter formatter) {
+ mFormatter = formatter;
+ }
+
+ /**
+ * Set the range of numbers allowed for the number picker. The current value
+ * will be automatically set to the start.
+ *
+ * @param start
+ * the start of the range (inclusive)
+ * @param end
+ * the end of the range (inclusive)
+ */
+ public void setRange(int start, int end) {
+ mStart = start;
+ mEnd = end;
+ mCurrent = start;
+ updateView();
+ }
+
+ /**
+ * Set the range of numbers allowed for the number picker. The current value
+ * will be automatically set to the start. Also provide a mapping for values
+ * used to display to the user.
+ *
+ * @param start
+ * the start of the range (inclusive)
+ * @param end
+ * the end of the range (inclusive)
+ * @param displayedValues
+ * the values displayed to the user.
+ */
+ public void setRange(int start, int end, String[] displayedValues) {
+ mDisplayedValues = displayedValues;
+ mStart = start;
+ mEnd = end;
+ mCurrent = start;
+ updateView();
+ }
+
+ public void setCurrent(int current) {
+ mCurrent = current;
+ updateView();
+ }
+
+ /**
+ * The speed (in milliseconds) at which the numbers will scroll when the the
+ * +/- buttons are longpressed. Default is 300ms.
+ */
+ public void setSpeed(long speed) {
+ mSpeed = speed;
+ }
+
+ public void onClick(View v) {
+
+ /*
+ * The text view may still have focus so clear it's focus which will
+ * trigger the on focus changed and any typed values to be pulled.
+ */
+ mText.clearFocus();
+
+ // now perform the increment/decrement
+ if (R.id.increment == v.getId()) {
+ changeCurrent(mCurrent + incrementBy, mSlideUpInAnimation,
+ mSlideUpOutAnimation);
+ } else if (R.id.decrement == v.getId()) {
+ changeCurrent(mCurrent - incrementBy, mSlideDownInAnimation,
+ mSlideDownOutAnimation);
+ }
+ }
+
+ private String formatNumber(int value) {
+ return (mFormatter != null) ? mFormatter.toString(value) : String
+ .valueOf(value);
+ }
+
+ private void changeCurrent(int current, Animation in, Animation out) {
+
+ // Wrap around the values if we go past the start or end
+ if (current > mEnd) {
+ current = mStart;
+ } else if (current < mStart) {
+ current = mEnd;
+ }
+ mPrevious = mCurrent;
+ mCurrent = current;
+ notifyChange();
+ updateView();
+ }
+
+ private void notifyChange() {
+ if (mListener != null) {
+ mListener.onChanged(this, mPrevious, mCurrent);
+ }
+ }
+
+ private void updateView() {
+
+ /*
+ * If we don't have displayed values then use the current number else
+ * find the correct value in the displayed values for the current
+ * number.
+ */
+ if (mDisplayedValues == null) {
+ mText.setText(formatNumber(mCurrent));
+ } else {
+ mText.setText(mDisplayedValues[mCurrent - mStart]);
+ }
+ }
+
+ private void validateCurrentView(CharSequence str) {
+ int val = getSelectedPos(str.toString());
+ if ((val >= mStart) && (val <= mEnd)) {
+ mPrevious = mCurrent;
+ mCurrent = val;
+ notifyChange();
+ }
+ updateView();
+ }
+
+ public void onFocusChange(View v, boolean hasFocus) {
+
+ /*
+ * When focus is lost check that the text field has valid values.
+ */
+ if (!hasFocus) {
+ String str = String.valueOf(((TextView) v).getText());
+ if ("".equals(str)) {
+
+ // Restore to the old value as we don't allow empty values
+ updateView();
+ } else {
+
+ // Check the new value and ensure it's in range
+ validateCurrentView(str);
+ }
+ }
+ }
+
+ /**
+ * We start the long click here but rely on the {@link NumberPickerButton}
+ * to inform us when the long click has ended.
+ */
+ public boolean onLongClick(View v) {
+
+ /*
+ * The text view may still have focus so clear it's focus which will
+ * trigger the on focus changed and any typed values to be pulled.
+ */
+ mText.clearFocus();
+
+ if (R.id.increment == v.getId()) {
+ mIncrement = true;
+ mHandler.post(mRunnable);
+ } else if (R.id.decrement == v.getId()) {
+ mDecrement = true;
+ mHandler.post(mRunnable);
+ }
+ return true;
+ }
+
+ public void cancelIncrement() {
+ mIncrement = false;
+ }
+
+ public void cancelDecrement() {
+ mDecrement = false;
+ }
+
+ private static final char[] DIGIT_CHARACTERS = new char[] { '0', '1', '2',
+ '3', '4', '5', '6', '7', '8', '9' };
+
+ private NumberPickerButton mIncrementButton;
+ private NumberPickerButton mDecrementButton;
+
+ private class NumberPickerInputFilter implements InputFilter {
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ if (mDisplayedValues == null) {
+ return mNumberInputFilter.filter(source, start, end, dest,
+ dstart, dend);
+ }
+ CharSequence filtered = String.valueOf(source.subSequence(start,
+ end));
+ String result = String.valueOf(dest.subSequence(0, dstart))
+ + filtered + dest.subSequence(dend, dest.length());
+ String str = String.valueOf(result).toLowerCase();
+ for (String val : mDisplayedValues) {
+ val = val.toLowerCase();
+ if (val.startsWith(str)) {
+ return filtered;
+ }
+ }
+ return "";
+ }
+ }
+
+ private class NumberRangeKeyListener extends NumberKeyListener {
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return DIGIT_CHARACTERS;
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+
+ CharSequence filtered = super.filter(source, start, end, dest,
+ dstart, dend);
+ if (filtered == null) {
+ filtered = source.subSequence(start, end);
+ }
+
+ String result = String.valueOf(dest.subSequence(0, dstart))
+ + filtered + dest.subSequence(dend, dest.length());
+
+ if ("".equals(result)) {
+ return result;
+ }
+ int val = getSelectedPos(result);
+
+ /*
+ * Ensure the user can't type in a value greater than the max
+ * allowed. We have to allow less than min as the user might want to
+ * delete some numbers and then type a new number.
+ */
+ if (val > mEnd) {
+ return "";
+ } else {
+ return filtered;
+ }
+ }
+ }
+
+ private int getSelectedPos(String str) {
+ if (mDisplayedValues == null) {
+ return Integer.parseInt(str);
+ } else {
+ for (int i = 0; i < mDisplayedValues.length; i++) {
+
+ /* Don't force the user to type in jan when ja will do */
+ str = str.toLowerCase();
+ if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
+ return mStart + i;
+ }
+ }
+
+ /*
+ * The user might have typed in a number into the month field i.e.
+ * 10 instead of OCT so support that too.
+ */
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException e) {
+
+ /* Ignore as if it's not a number we don't care */
+ }
+ }
+ return mStart;
+ }
+
+ /**
+ * @return the current value.
+ */
+ public int getCurrent() {
+ return mCurrent;
+ }
+}
\ No newline at end of file
diff --git a/trunk/src/com/timsu/astrid/widget/NumberPickerButton.java b/trunk/src/com/timsu/astrid/widget/NumberPickerButton.java
new file mode 100644
index 000000000..d637f29f1
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/widget/NumberPickerButton.java
@@ -0,0 +1,70 @@
+package com.timsu.astrid.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.widget.ImageButton;
+
+import com.timsu.astrid.R;
+
+/**
+ * This class exists purely to cancel long click events.
+ */
+public class NumberPickerButton extends ImageButton {
+
+ private NumberPicker mNumberPicker;
+
+ public NumberPickerButton(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public NumberPickerButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NumberPickerButton(Context context) {
+ super(context);
+ }
+
+ public void setNumberPicker(NumberPicker picker) {
+ mNumberPicker = picker;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ cancelLongpressIfRequired(event);
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ cancelLongpressIfRequired(event);
+ return super.onTrackballEvent(event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER)
+ || (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ cancelLongpress();
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private void cancelLongpressIfRequired(MotionEvent event) {
+ if ((event.getAction() == MotionEvent.ACTION_CANCEL)
+ || (event.getAction() == MotionEvent.ACTION_UP)) {
+ cancelLongpress();
+ }
+ }
+
+ private void cancelLongpress() {
+ if (R.id.increment == getId()) {
+ mNumberPicker.cancelIncrement();
+ } else if (R.id.decrement == getId()) {
+ mNumberPicker.cancelDecrement();
+ }
+ }
+}
\ No newline at end of file
diff --git a/trunk/src/com/timsu/astrid/widget/NumberPickerDialog.java b/trunk/src/com/timsu/astrid/widget/NumberPickerDialog.java
new file mode 100644
index 000000000..f534efa8c
--- /dev/null
+++ b/trunk/src/com/timsu/astrid/widget/NumberPickerDialog.java
@@ -0,0 +1,51 @@
+package com.timsu.astrid.widget;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.timsu.astrid.R;
+
+public class NumberPickerDialog extends AlertDialog implements OnClickListener {
+
+ public interface OnNumberPickedListener {
+ void onNumberPicked(NumberPicker view, int number);
+ }
+
+ private final NumberPicker mPicker;
+ private final OnNumberPickedListener mCallback;
+
+ public NumberPickerDialog(Context context, OnNumberPickedListener callBack,
+ String title, int initialValue, int incrementBy, int start, int end) {
+ super(context);
+ mCallback = callBack;
+
+ setButton(context.getText(android.R.string.ok), this);
+ setButton2(context.getText(android.R.string.cancel), (OnClickListener) null);
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.number_picker_dialog, null);
+ setView(view);
+
+ setTitle(title);
+ mPicker = (NumberPicker) view.findViewById(R.id.numberPicker);
+ mPicker.setCurrent(initialValue);
+ mPicker.setIncrementBy(incrementBy);
+ mPicker.setRange(start, end);
+ }
+
+ public void setInitialValue(int initialValue) {
+ mPicker.setCurrent(initialValue);
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (mCallback != null) {
+ mPicker.clearFocus();
+ mCallback.onNumberPicked(mPicker, mPicker.getCurrent());
+ }
+ }
+}